1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-06-07 19:59:14 -04:00

refactor(Hyprland): Update Lua migration and keybind writes

- emit native hl.dsp.* dispatchers for generated Lua keybinds
- keep legacy hyprland.conf installs read-only but preserved until dms setup migration
This commit is contained in:
purian23
2026-05-30 23:07:06 -04:00
parent 389fffaf64
commit a265625851
20 changed files with 1056 additions and 109 deletions
+8
View File
@@ -56,6 +56,8 @@ func init() {
type IncludeResult struct { type IncludeResult struct {
Exists bool `json:"exists"` Exists bool `json:"exists"`
Included bool `json:"included"` Included bool `json:"included"`
ConfigFormat string `json:"configFormat,omitempty"`
ReadOnly bool `json:"readOnly,omitempty"`
} }
func runResolveInclude(cmd *cobra.Command, args []string) { func runResolveInclude(cmd *cobra.Command, args []string) {
@@ -106,6 +108,8 @@ func checkHyprlandInclude(filename string) (IncludeResult, error) {
mainLua := filepath.Join(configDir, "hyprland.lua") mainLua := filepath.Join(configDir, "hyprland.lua")
if _, err := os.Stat(mainLua); err == nil { if _, err := os.Stat(mainLua); err == nil {
result.ConfigFormat = "lua"
result.ReadOnly = false
processedLua := make(map[string]bool) processedLua := make(map[string]bool)
if luaconfig.RequiresTarget(mainLua, targetAbs, processedLua) { if luaconfig.RequiresTarget(mainLua, targetAbs, processedLua) {
result.Included = true result.Included = true
@@ -115,6 +119,10 @@ func checkHyprlandInclude(filename string) (IncludeResult, error) {
mainConf := filepath.Join(configDir, "hyprland.conf") mainConf := filepath.Join(configDir, "hyprland.conf")
if _, err := os.Stat(mainConf); err == nil { if _, err := os.Stat(mainConf); err == nil {
if result.ConfigFormat == "" {
result.ConfigFormat = "hyprlang"
result.ReadOnly = true
}
processed := make(map[string]bool) processed := make(map[string]bool)
if hyprlandFindIncludeHyprlang(mainConf, targetRel, processed) { if hyprlandFindIncludeHyprlang(mainConf, targetRel, processed) {
result.Included = true result.Included = true
+4
View File
@@ -600,6 +600,10 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystem
return result, result.Error return result, result.Error
} }
CleanupStrayHyprlandConfFile(func(format string, v ...any) {
cd.log(fmt.Sprintf(format, v...))
})
result.Deployed = true result.Deployed = true
cd.log("Successfully deployed Hyprland configuration") cd.log("Successfully deployed Hyprland configuration")
return result, nil return result, nil
+15 -3
View File
@@ -20,13 +20,17 @@ func TestCleanupStrayHyprlandConfFile(t *testing.T) {
td := t.TempDir() td := t.TempDir()
t.Setenv("HOME", td) t.Setenv("HOME", td)
configDir := filepath.Join(td, ".config", "hypr") configDir := filepath.Join(td, ".config", "hypr")
require.NoError(t, os.MkdirAll(configDir, 0o755)) dmsDir := filepath.Join(configDir, "dms")
require.NoError(t, os.MkdirAll(dmsDir, 0o755))
confPath := filepath.Join(configDir, "hyprland.conf") confPath := filepath.Join(configDir, "hyprland.conf")
dmsConfPath := filepath.Join(dmsDir, "colors.conf")
require.NoError(t, os.WriteFile(confPath, []byte("# legacy user config\n"), 0o644)) require.NoError(t, os.WriteFile(confPath, []byte("# legacy user config\n"), 0o644))
require.NoError(t, os.WriteFile(dmsConfPath, []byte("$primary = rgba(d0bcffFF)\n"), 0o644))
CleanupStrayHyprlandConfFile(nil) CleanupStrayHyprlandConfFile(nil)
assert.FileExists(t, confPath, "must not touch hyprland.conf when user has not migrated") assert.FileExists(t, confPath, "must not touch hyprland.conf when user has not migrated")
assert.FileExists(t, dmsConfPath, "must not touch dms/*.conf when user has not migrated")
assert.NoDirExists(t, filepath.Join(configDir, hyprlandBackupDirName)) assert.NoDirExists(t, filepath.Join(configDir, hyprlandBackupDirName))
}) })
@@ -34,20 +38,25 @@ func TestCleanupStrayHyprlandConfFile(t *testing.T) {
td := t.TempDir() td := t.TempDir()
t.Setenv("HOME", td) t.Setenv("HOME", td)
configDir := filepath.Join(td, ".config", "hypr") configDir := filepath.Join(td, ".config", "hypr")
require.NoError(t, os.MkdirAll(configDir, 0o755)) dmsDir := filepath.Join(configDir, "dms")
require.NoError(t, os.MkdirAll(dmsDir, 0o755))
luaPath := filepath.Join(configDir, "hyprland.lua") luaPath := filepath.Join(configDir, "hyprland.lua")
require.NoError(t, os.WriteFile(luaPath, []byte("-- dms managed\n"), 0o644)) require.NoError(t, os.WriteFile(luaPath, []byte("-- dms managed\n"), 0o644))
confPath := filepath.Join(configDir, "hyprland.conf") confPath := filepath.Join(configDir, "hyprland.conf")
dmsConfPath := filepath.Join(dmsDir, "colors.conf")
require.NoError(t, os.WriteFile(confPath, []byte("# autogen\n"), 0o644)) require.NoError(t, os.WriteFile(confPath, []byte("# autogen\n"), 0o644))
require.NoError(t, os.WriteFile(dmsConfPath, []byte("$primary = rgba(d0bcffFF)\n"), 0o644))
CleanupStrayHyprlandConfFile(nil) CleanupStrayHyprlandConfFile(nil)
assert.NoFileExists(t, confPath) assert.NoFileExists(t, confPath)
assert.NoFileExists(t, dmsConfPath)
assert.FileExists(t, luaPath) assert.FileExists(t, luaPath)
entries, err := os.ReadDir(filepath.Join(configDir, hyprlandBackupDirName)) entries, err := os.ReadDir(filepath.Join(configDir, hyprlandBackupDirName))
require.NoError(t, err) require.NoError(t, err)
require.Len(t, entries, 1) require.Len(t, entries, 1)
assert.FileExists(t, filepath.Join(configDir, hyprlandBackupDirName, entries[0].Name(), "hyprland.conf")) assert.FileExists(t, filepath.Join(configDir, hyprlandBackupDirName, entries[0].Name(), "hyprland.conf"))
assert.FileExists(t, filepath.Join(configDir, hyprlandBackupDirName, entries[0].Name(), "dms", "colors.conf"))
}) })
} }
@@ -404,6 +413,7 @@ general {
dmsDir := filepath.Join(td, ".config", "hypr", "dms") dmsDir := filepath.Join(td, ".config", "hypr", "dms")
require.NoError(t, os.MkdirAll(dmsDir, 0o755)) 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, "binds.conf"), []byte("bind = SUPER, T, exec, foot\n"), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(dmsDir, "colors.conf"), []byte("$primary = rgba(d0bcffFF)\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(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(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)) require.NoError(t, os.WriteFile(filepath.Join(dmsDir, "binds.conf.backup.old"), []byte("old dms backup\n"), 0o644))
@@ -423,10 +433,12 @@ general {
assert.Contains(t, result.BackupPath, hyprlandBackupDirName) assert.Contains(t, result.BackupPath, hyprlandBackupDirName)
assert.NoFileExists(t, hyprPath) 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", "binds.conf"))
assert.FileExists(t, filepath.Join(filepath.Dir(result.BackupPath), "dms", "colors.conf"))
assert.FileExists(t, filepath.Join(filepath.Dir(result.BackupPath), "dms", "cursor.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), "hyprland.conf.backup.old"))
assert.FileExists(t, filepath.Join(filepath.Dir(result.BackupPath), "dms", "binds.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, "binds.conf"))
assert.NoFileExists(t, filepath.Join(dmsDir, "colors.conf"))
assert.NoFileExists(t, filepath.Join(dmsDir, "cursor.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(filepath.Dir(hyprPath), "hyprland.conf.backup.old"))
assert.NoFileExists(t, filepath.Join(dmsDir, "binds.conf.backup.old")) assert.NoFileExists(t, filepath.Join(dmsDir, "binds.conf.backup.old"))
@@ -485,7 +497,7 @@ general {
managed, err := os.ReadFile(filepath.Join(dmsDir, "binds.lua")) managed, err := os.ReadFile(filepath.Join(dmsDir, "binds.lua"))
require.NoError(t, err) 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 + 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 })`) assert.Contains(t, string(managed), `hl.bind("SUPER + minus", hl.dsp.window.resize({ x = -100, y = 0, relative = true }), { repeating = true })`)
user, err := os.ReadFile(filepath.Join(dmsDir, "binds-user.lua")) user, err := os.ReadFile(filepath.Join(dmsDir, "binds-user.lua"))
require.NoError(t, err) require.NoError(t, err)
+5 -5
View File
@@ -140,7 +140,7 @@ hl.bind("SUPER + bracketright", hl.dsp.layout("preselect r"))
-- === Sizing & Layout === -- === Sizing & Layout ===
hl.bind("SUPER + R", hl.dsp.layout("togglesplit")) hl.bind("SUPER + R", hl.dsp.layout("togglesplit"))
hl.bind("SUPER + CTRL + F", hl.dsp.exec_cmd([[hyprctl dispatch resizeactive exact 100% 100%]])) hl.bind("SUPER + CTRL + F", hl.dsp.window.fullscreen({ mode = "maximized", action = "set" }))
-- === Move/resize windows with mainMod + LMB/RMB and dragging === -- === Move/resize windows with mainMod + LMB/RMB and dragging ===
hl.bind("SUPER + mouse:272", hl.dsp.window.drag(), { mouse = true, description = "Move window" }) hl.bind("SUPER + mouse:272", hl.dsp.window.drag(), { mouse = true, description = "Move window" })
@@ -150,10 +150,10 @@ hl.bind("SUPER + code:20", hl.dsp.window.resize({ x = -100, y = 0, relative = tr
hl.bind("SUPER + code:21", hl.dsp.window.resize({ x = 100, y = 0, relative = true }), { description = "Shrink window left" }) hl.bind("SUPER + code:21", hl.dsp.window.resize({ x = 100, y = 0, relative = true }), { description = "Shrink window left" })
-- === Manual Sizing === -- === Manual Sizing ===
hl.bind("SUPER + minus", hl.dsp.exec_cmd([[hyprctl dispatch resizeactive -10% 0]]), { repeating = true }) hl.bind("SUPER + minus", hl.dsp.window.resize({ x = -100, y = 0, relative = true }), { repeating = true })
hl.bind("SUPER + equal", hl.dsp.exec_cmd([[hyprctl dispatch resizeactive 10% 0]]), { repeating = true }) hl.bind("SUPER + equal", hl.dsp.window.resize({ x = 100, y = 0, relative = true }), { repeating = true })
hl.bind("SUPER + SHIFT + minus", hl.dsp.exec_cmd([[hyprctl dispatch resizeactive 0 -10%]]), { repeating = true }) hl.bind("SUPER + SHIFT + minus", hl.dsp.window.resize({ x = 0, y = -100, relative = true }), { repeating = true })
hl.bind("SUPER + SHIFT + equal", hl.dsp.exec_cmd([[hyprctl dispatch resizeactive 0 10%]]), { repeating = true }) hl.bind("SUPER + SHIFT + equal", hl.dsp.window.resize({ x = 0, y = 100, relative = true }), { repeating = true })
-- === Screenshots === -- === Screenshots ===
hl.bind("Print", hl.dsp.exec_cmd("dms screenshot")) hl.bind("Print", hl.dsp.exec_cmd("dms screenshot"))
+34 -11
View File
@@ -138,11 +138,9 @@ func readExistingHyprlandConfig(configDir string) (data string, sourcePath strin
return "", "", nil return "", "", nil
} }
// CleanupStrayHyprlandConfFile moves a stray ~/.config/hypr/hyprland.conf // CleanupStrayHyprlandConfFile moves stray ~/.config/hypr/hyprland.conf and
// into .dms-backups/<timestamp>/ only when hyprland.lua also exists, which // top-level ~/.config/hypr/dms/*.conf files into .dms-backups/<timestamp>/ only
// proves Lua is the live config and the .conf is an autogen Hyprland 0.55 // when hyprland.lua also exists as the live config.
// produced when launched without -c. If only hyprland.conf exists, the user
// has not migrated and we must leave their config alone.
func CleanupStrayHyprlandConfFile(logFn func(format string, v ...any)) { func CleanupStrayHyprlandConfFile(logFn func(format string, v ...any)) {
if os.Getenv("HYPRLAND_INSTANCE_SIGNATURE") == "" { if os.Getenv("HYPRLAND_INSTANCE_SIGNATURE") == "" {
return return
@@ -156,19 +154,44 @@ func CleanupStrayHyprlandConfFile(logFn func(format string, v ...any)) {
if _, err := os.Stat(luaPath); err != nil { if _, err := os.Stat(luaPath); err != nil {
return return
} }
var strayPaths []string
confPath := filepath.Join(configDir, "hyprland.conf") confPath := filepath.Join(configDir, "hyprland.conf")
if _, err := os.Stat(confPath); err != nil { if info, err := os.Lstat(confPath); err == nil && !info.IsDir() {
strayPaths = append(strayPaths, confPath)
}
dmsConfPaths, err := filepath.Glob(filepath.Join(configDir, "dms", "*.conf"))
if err == nil {
for _, p := range dmsConfPaths {
if info, err := os.Lstat(p); err == nil && !info.IsDir() {
strayPaths = append(strayPaths, p)
}
}
}
if len(strayPaths) == 0 {
return return
} }
ts := time.Now().Format("2006-01-02_15-04-05") ts := time.Now().Format("2006-01-02_15-04-05")
dst := filepath.Join(configDir, hyprlandBackupDirName, ts, "hyprland.conf") moved := 0
if err := moveHyprlandConfigFile(confPath, dst); err != nil { for _, src := range strayPaths {
rel, err := filepath.Rel(configDir, src)
if err != nil {
rel = filepath.Base(src)
}
dst := filepath.Join(configDir, hyprlandBackupDirName, ts, rel)
if err := moveHyprlandConfigFile(src, dst); err != nil {
if logFn != nil { if logFn != nil {
logFn("Could not move stray hyprland.conf: %v", err) logFn("Could not move stray Hyprland conf file %s: %v", src, err)
} }
return continue
} }
moved++
if logFn != nil { if logFn != nil {
logFn("Moved stray hyprland.conf to %s", dst) logFn("Moved stray Hyprland conf file to %s", dst)
}
}
if moved > 0 && logFn != nil {
logFn("Moved %d stray Hyprland conf file(s) out of the active Lua config tree", moved)
} }
} }
+403 -22
View File
@@ -68,6 +68,8 @@ func (h *HyprlandProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
Effective: result.DMSStatus.Effective, Effective: result.DMSStatus.Effective,
OverriddenBy: result.DMSStatus.OverriddenBy, OverriddenBy: result.DMSStatus.OverriddenBy,
StatusMessage: result.DMSStatus.StatusMessage, StatusMessage: result.DMSStatus.StatusMessage,
ConfigFormat: result.DMSStatus.ConfigFormat,
ReadOnly: result.DMSStatus.ReadOnly,
} }
} }
@@ -219,6 +221,9 @@ func (h *HyprlandProvider) validateAction(action string) error {
} }
func (h *HyprlandProvider) SetBind(key, action, description string, options map[string]any) error { func (h *HyprlandProvider) SetBind(key, action, description string, options map[string]any) error {
if err := h.ensureWritableConfig(); err != nil {
return err
}
if err := h.validateAction(action); err != nil { if err := h.validateAction(action); err != nil {
return err return err
} }
@@ -242,9 +247,10 @@ func (h *HyprlandProvider) SetBind(key, action, description string, options map[
} }
} }
normalizedKey := strings.ToLower(key) canonicalKey := canonicalHyprlandOverrideKey(key)
normalizedKey := hyprlandOverrideMapKey(canonicalKey)
existingBinds[normalizedKey] = &hyprlandOverrideBind{ existingBinds[normalizedKey] = &hyprlandOverrideBind{
Key: key, Key: canonicalKey,
Action: action, Action: action,
Description: description, Description: description,
Flags: flags, Flags: flags,
@@ -255,21 +261,28 @@ func (h *HyprlandProvider) SetBind(key, action, description string, options map[
} }
func (h *HyprlandProvider) RemoveBind(key string) error { func (h *HyprlandProvider) RemoveBind(key string) error {
if err := h.ensureWritableConfig(); err != nil {
return err
}
existingBinds, err := h.loadOverrideBinds() existingBinds, err := h.loadOverrideBinds()
if err != nil { if err != nil {
return nil return nil
} }
normalizedKey := strings.ToLower(key) canonicalKey := canonicalHyprlandOverrideKey(key)
existingBinds[normalizedKey] = &hyprlandOverrideBind{Key: key, Unbind: true} normalizedKey := hyprlandOverrideMapKey(canonicalKey)
existingBinds[normalizedKey] = &hyprlandOverrideBind{Key: canonicalKey, Unbind: true}
return h.writeOverrideBinds(existingBinds) return h.writeOverrideBinds(existingBinds)
} }
func (h *HyprlandProvider) ResetBind(key string) error { func (h *HyprlandProvider) ResetBind(key string) error {
if err := h.ensureWritableConfig(); err != nil {
return err
}
existingBinds, err := h.loadOverrideBinds() existingBinds, err := h.loadOverrideBinds()
if err != nil { if err != nil {
return nil return nil
} }
normalizedKey := strings.ToLower(key) normalizedKey := hyprlandOverrideMapKey(key)
delete(existingBinds, normalizedKey) delete(existingBinds, normalizedKey)
return h.writeOverrideBinds(existingBinds) return h.writeOverrideBinds(existingBinds)
} }
@@ -284,10 +297,46 @@ type hyprlandOverrideBind struct {
Unbind bool Unbind bool
} }
func (h *HyprlandProvider) ensureWritableConfig() error {
if h.isLegacyConfigReadOnly() {
return fmt.Errorf("hyprland legacy conf configs are read-only; run dms setup to migrate to Lua before editing keybinds")
}
return nil
}
func (h *HyprlandProvider) isLegacyConfigReadOnly() bool {
expanded, err := utils.ExpandPath(h.configPath)
if err != nil {
expanded = h.configPath
}
luaPath := filepath.Join(expanded, "hyprland.lua")
if st, err := os.Stat(luaPath); err == nil && st.Mode().IsRegular() {
return false
}
confPath := filepath.Join(expanded, "hyprland.conf")
if st, err := os.Stat(confPath); err == nil && st.Mode().IsRegular() {
return true
}
return false
}
func (h *HyprlandProvider) loadOverrideBinds() (map[string]*hyprlandOverrideBind, error) { func (h *HyprlandProvider) loadOverrideBinds() (map[string]*hyprlandOverrideBind, error) {
return readLuaOrHyprlangOverride(h.GetOverridePath()) return readLuaOrHyprlangOverride(h.GetOverridePath())
} }
func canonicalHyprlandOverrideKey(key string) string {
trimmed := strings.TrimSpace(key)
normalized := luaKeyComboToInternalKey(trimmed)
if normalized == "" {
return trimmed
}
return normalized
}
func hyprlandOverrideMapKey(key string) string {
return strings.ToLower(canonicalHyprlandOverrideKey(key))
}
func (h *HyprlandProvider) getBindSortPriority(action string) int { func (h *HyprlandProvider) getBindSortPriority(action string) int {
switch { switch {
case strings.HasPrefix(action, "exec") && strings.Contains(action, "dms"): case strings.HasPrefix(action, "exec") && strings.Contains(action, "dms"):
@@ -368,25 +417,355 @@ func normalizeLuaBindKeyPart(part string) string {
return part return part
} }
type luaField struct {
name string
value string
}
func luaDispatcherTableCall(funcName string, fields ...luaField) string {
parts := make([]string, 0, len(fields))
for _, field := range fields {
if field.name == "" || field.value == "" {
continue
}
parts = append(parts, field.name+" = "+field.value)
}
return fmt.Sprintf(`%s({ %s })`, funcName, strings.Join(parts, ", "))
}
func luaStringField(name, value string) luaField {
return luaField{name: name, value: strconv.Quote(strings.TrimSpace(value))}
}
func luaBoolField(name string, value bool) luaField {
if value {
return luaField{name: name, value: "true"}
}
return luaField{name: name, value: "false"}
}
func luaNumberOrStringField(name, value string) luaField {
value = strings.TrimSpace(value)
if isBareLuaNumber(value) {
return luaField{name: name, value: value}
}
return luaStringField(name, value)
}
func isBareLuaNumber(value string) bool {
if value == "" || strings.HasPrefix(value, "+") {
return false
}
if value[0] == '-' {
value = value[1:]
}
if value == "" {
return false
}
digitsBeforeDot := 0
i := 0
for i < len(value) && value[i] >= '0' && value[i] <= '9' {
digitsBeforeDot++
i++
}
digitsAfterDot := 0
if i < len(value) && value[i] == '.' {
i++
for i < len(value) && value[i] >= '0' && value[i] <= '9' {
digitsAfterDot++
i++
}
}
return i == len(value) && (digitsBeforeDot > 0 || digitsAfterDot > 0)
}
func splitHyprlandAction(action string) (dispatcher, params string) {
action = strings.TrimSpace(action)
if action == "" {
return "", ""
}
idx := strings.IndexFunc(action, func(r rune) bool {
return r == ' ' || r == '\t' || r == '\r' || r == '\n'
})
if idx < 0 {
return strings.ToLower(action), ""
}
return strings.ToLower(strings.TrimSpace(action[:idx])), strings.TrimSpace(action[idx+1:])
}
func firstParam(params string) (head, rest string) {
params = strings.TrimSpace(params)
if params == "" {
return "", ""
}
fields := strings.Fields(params)
if len(fields) == 0 {
return "", ""
}
head = fields[0]
rest = strings.TrimSpace(strings.TrimPrefix(params, head))
return head, rest
}
func xyParams(params string) (x, y string, relative bool, ok bool) {
fields := strings.Fields(params)
if len(fields) > 0 && strings.EqualFold(fields[0], "exact") {
relative = false
fields = fields[1:]
} else {
relative = true
}
if len(fields) < 2 {
return "", "", relative, false
}
return fields[0], fields[1], relative, true
}
func dispatcherWorkspaceMove(params string, follow *bool) string {
workspace, window := firstParam(params)
if workspace == "" {
return ""
}
fields := []luaField{luaStringField("workspace", workspace)}
if follow != nil {
fields = append(fields, luaBoolField("follow", *follow))
}
if window != "" {
fields = append(fields, luaStringField("window", window))
}
return luaDispatcherTableCall("hl.dsp.window.move", fields...)
}
func dispatcherActiveMoveResize(funcName, params string) string {
x, y, relative, ok := xyParams(params)
if !ok {
return ""
}
if !isBareLuaNumber(x) || !isBareLuaNumber(y) {
return ""
}
return luaDispatcherTableCall(funcName,
luaNumberOrStringField("x", x),
luaNumberOrStringField("y", y),
luaBoolField("relative", relative),
)
}
func luaActionStringFromKnownHyprlandAction(action string) (string, bool) {
dispatcher, params := splitHyprlandAction(action)
switch dispatcher {
case "spawn", "exec":
return fmt.Sprintf(`hl.dsp.exec_cmd(%s)`, strconv.Quote(params)), true
case "killactive":
return `hl.dsp.window.kill()`, true
case "closewindow":
if params == "" {
return `hl.dsp.window.close()`, true
}
return fmt.Sprintf(`hl.dsp.window.close(%s)`, strconv.Quote(params)), true
case "killwindow":
if params == "" {
return `hl.dsp.window.kill()`, true
}
return fmt.Sprintf(`hl.dsp.window.kill(%s)`, strconv.Quote(params)), true
case "togglefloating":
return `hl.dsp.window.float({ action = "toggle" })`, true
case "setfloating":
return `hl.dsp.window.float({ action = "set" })`, true
case "settiled":
return `hl.dsp.window.float({ action = "unset" })`, true
case "fullscreen":
mode := strings.TrimSpace(params)
switch mode {
case "", "0":
return `hl.dsp.window.fullscreen({ mode = "fullscreen", action = "toggle" })`, true
case "1":
return `hl.dsp.window.fullscreen({ mode = "maximized", action = "toggle" })`, true
}
case "fullscreenstate":
internal, rest := firstParam(params)
client, _ := firstParam(rest)
if internal != "" && client != "" {
return luaDispatcherTableCall("hl.dsp.window.fullscreen_state",
luaNumberOrStringField("internal", internal),
luaNumberOrStringField("client", client),
), true
}
case "pin":
if params == "" {
return `hl.dsp.window.pin()`, true
}
case "centerwindow":
return `hl.dsp.window.center()`, true
case "resizewindow":
return `hl.dsp.window.resize()`, true
case "movewindow":
if params == "" {
return `hl.dsp.window.drag()`, true
}
if monitor, ok := strings.CutPrefix(params, "mon:"); ok {
return luaDispatcherTableCall("hl.dsp.window.move", luaStringField("monitor", monitor)), true
}
return luaDispatcherTableCall("hl.dsp.window.move", luaStringField("direction", params)), true
case "swapwindow":
if params == "" {
return "", false
}
return luaDispatcherTableCall("hl.dsp.window.swap", luaStringField("direction", params)), true
case "resizeactive":
if expr := dispatcherActiveMoveResize("hl.dsp.window.resize", params); expr != "" {
return expr, true
}
case "moveactive":
if expr := dispatcherActiveMoveResize("hl.dsp.window.move", params); expr != "" {
return expr, true
}
case "workspace":
if params == "" {
return "", false
}
return luaDispatcherTableCall("hl.dsp.focus", luaStringField("workspace", params)), true
case "focusworkspaceoncurrentmonitor":
if params == "" {
return "", false
}
return luaDispatcherTableCall("hl.dsp.focus", luaStringField("workspace", params), luaBoolField("on_current_monitor", true)), true
case "movetoworkspace":
if expr := dispatcherWorkspaceMove(params, nil); expr != "" {
return expr, true
}
case "movetoworkspacesilent":
follow := false
if expr := dispatcherWorkspaceMove(params, &follow); expr != "" {
return expr, true
}
case "togglespecialworkspace":
if params == "" {
return `hl.dsp.workspace.toggle_special()`, true
}
return fmt.Sprintf(`hl.dsp.workspace.toggle_special(%s)`, strconv.Quote(params)), true
case "renameworkspace":
workspace, name := firstParam(params)
if workspace != "" {
fields := []luaField{luaStringField("workspace", workspace)}
if name != "" {
fields = append(fields, luaStringField("name", name))
}
return luaDispatcherTableCall("hl.dsp.workspace.rename", fields...), true
}
case "movecurrentworkspacetomonitor":
if params != "" {
return luaDispatcherTableCall("hl.dsp.workspace.move", luaStringField("monitor", params)), true
}
case "moveworkspacetomonitor":
workspace, monitor := firstParam(params)
if workspace != "" && monitor != "" {
return luaDispatcherTableCall("hl.dsp.workspace.move", luaStringField("workspace", workspace), luaStringField("monitor", monitor)), true
}
case "swapactiveworkspaces":
monitor1, rest := firstParam(params)
monitor2, _ := firstParam(rest)
if monitor1 != "" && monitor2 != "" {
return luaDispatcherTableCall("hl.dsp.workspace.swap_monitors", luaStringField("monitor1", monitor1), luaStringField("monitor2", monitor2)), true
}
case "movefocus":
if params != "" {
return luaDispatcherTableCall("hl.dsp.focus", luaStringField("direction", params)), true
}
case "focusmonitor":
if params != "" {
return luaDispatcherTableCall("hl.dsp.focus", luaStringField("monitor", params)), true
}
case "focuswindow":
if params != "" {
return luaDispatcherTableCall("hl.dsp.focus", luaStringField("window", params)), true
}
case "focuscurrentorlast":
return `hl.dsp.focus({ last = true })`, true
case "focusurgentorlast":
return `hl.dsp.focus({ urgent_or_last = true })`, true
case "layoutmsg":
if params != "" {
return fmt.Sprintf(`hl.dsp.layout(%s)`, strconv.Quote(params)), true
}
case "alterzorder":
mode, window := firstParam(params)
if mode != "" {
fields := []luaField{luaStringField("mode", mode)}
if window != "" {
fields = append(fields, luaStringField("window", window))
}
return luaDispatcherTableCall("hl.dsp.window.alter_zorder", fields...), true
}
case "setprop":
window, rest := firstParam(params)
prop, value := firstParam(rest)
if window != "" && prop != "" && value != "" {
return luaDispatcherTableCall("hl.dsp.window.set_prop",
luaStringField("window", window),
luaStringField("prop", prop),
luaStringField("value", value),
), true
}
case "dpms":
dpmsAction := strings.TrimSpace(params)
switch dpmsAction {
case "on":
dpmsAction = "enable"
case "off":
dpmsAction = "disable"
}
if dpmsAction == "" {
return `hl.dsp.dpms({})`, true
}
return luaDispatcherTableCall("hl.dsp.dpms", luaStringField("action", dpmsAction)), true
case "exit":
return `hl.dsp.exit()`, true
case "submap":
return fmt.Sprintf(`hl.dsp.submap(%s)`, strconv.Quote(params)), true
case "global":
return fmt.Sprintf(`hl.dsp.global(%s)`, strconv.Quote(params)), true
case "event":
return fmt.Sprintf(`hl.dsp.event(%s)`, strconv.Quote(params)), true
case "pass":
if params == "" {
return `hl.dsp.pass({})`, true
}
return luaDispatcherTableCall("hl.dsp.pass", luaStringField("window", params)), true
case "sendshortcut":
mod, rest := firstParam(params)
key, window := firstParam(rest)
if mod != "" && key != "" {
fields := []luaField{luaStringField("mods", mod), luaStringField("key", key)}
if window != "" {
fields = append(fields, luaStringField("window", window))
}
return luaDispatcherTableCall("hl.dsp.send_shortcut", fields...), true
}
case "sendkeystate":
mod, rest := firstParam(params)
key, rest := firstParam(rest)
state, window := firstParam(rest)
if mod != "" && key != "" && state != "" {
fields := []luaField{luaStringField("mods", mod), luaStringField("key", key), luaStringField("state", state)}
if window != "" {
fields = append(fields, luaStringField("window", window))
}
return luaDispatcherTableCall("hl.dsp.send_key_state", fields...), true
}
case "togglegroup":
return `hl.dsp.group.toggle()`, true
}
return "", false
}
func luaActionStringFromHyprlangAction(action string) string { func luaActionStringFromHyprlangAction(action string) string {
action = strings.TrimSpace(action) action = strings.TrimSpace(action)
if strings.HasPrefix(action, "spawn ") { if expr, ok := luaActionStringFromKnownHyprlandAction(action); ok {
return fmt.Sprintf(`hl.dsp.exec_cmd(%s)`, strconv.Quote(strings.TrimSpace(strings.TrimPrefix(action, "spawn ")))) return expr
} }
if strings.HasPrefix(action, "exec ") {
return fmt.Sprintf(`hl.dsp.exec_cmd(%s)`, strconv.Quote(strings.TrimPrefix(action, "exec ")))
}
switch action {
case "killactive":
return `hl.dsp.window.kill()`
case "togglefloating":
return `hl.dsp.window.float({ action = "toggle" })`
case "exit":
return `hl.dsp.exit()`
default:
return fmt.Sprintf(`hl.dsp.exec_cmd(%s)`, strconv.Quote("hyprctl dispatch "+action)) return fmt.Sprintf(`hl.dsp.exec_cmd(%s)`, strconv.Quote("hyprctl dispatch "+action))
} }
}
func luaExprToInternalAction(expr string) string { func luaExprToInternalAction(expr string) string {
d, p := luaExprToDispatcherParams(expr) d, p := luaExprToDispatcherParams(expr)
@@ -498,11 +877,12 @@ func readLuaOrHyprlangOverride(path string) (map[string]*hyprlandOverrideBind, e
continue continue
} }
if key, ok := parseLuaUnbindLine(line); ok { if key, ok := parseLuaUnbindLine(line); ok {
pendingUnbinds[strings.ToLower(key)] = key pendingUnbinds[hyprlandOverrideMapKey(key)] = canonicalHyprlandOverrideKey(key)
continue continue
} }
if kb, ok := parseLuaBindOverrideLine(line); ok { if kb, ok := parseLuaBindOverrideLine(line); ok {
normalizedKey := strings.ToLower(kb.Key) kb.Key = canonicalHyprlandOverrideKey(kb.Key)
normalizedKey := hyprlandOverrideMapKey(kb.Key)
binds[normalizedKey] = kb binds[normalizedKey] = kb
delete(pendingUnbinds, normalizedKey) delete(pendingUnbinds, normalizedKey)
continue continue
@@ -520,7 +900,8 @@ func readLuaOrHyprlangOverride(path string) (map[string]*hyprlandOverrideBind, e
action = kb.Dispatcher + " " + kb.Params action = kb.Dispatcher + " " + kb.Params
} }
flags := kb.Flags flags := kb.Flags
normalizedKey := strings.ToLower(keyStr) keyStr = canonicalHyprlandOverrideKey(keyStr)
normalizedKey := hyprlandOverrideMapKey(keyStr)
binds[normalizedKey] = &hyprlandOverrideBind{ binds[normalizedKey] = &hyprlandOverrideBind{
Key: keyStr, Key: keyStr,
Action: action, Action: action,
@@ -54,6 +54,8 @@ type HyprlandParser struct {
dmsProcessed bool dmsProcessed bool
removedKeys map[string]bool // bare hl.unbind targets (negative overrides) removedKeys map[string]bool // bare hl.unbind targets (negative overrides)
defaultDMSKeys map[string]bool // keys present in dms/binds.{lua,conf} defaultDMSKeys map[string]bool // keys present in dms/binds.{lua,conf}
configFormat string
readOnly bool
} }
func NewHyprlandParser(configDir string) *HyprlandParser { func NewHyprlandParser(configDir string) *HyprlandParser {
@@ -310,6 +312,8 @@ type HyprlandDMSStatus struct {
Effective bool Effective bool
OverriddenBy int OverriddenBy int
StatusMessage string StatusMessage string
ConfigFormat string
ReadOnly bool
} }
func (p *HyprlandParser) buildDMSStatus() *HyprlandDMSStatus { func (p *HyprlandParser) buildDMSStatus() *HyprlandDMSStatus {
@@ -319,6 +323,8 @@ func (p *HyprlandParser) buildDMSStatus() *HyprlandDMSStatus {
IncludePosition: p.dmsIncludePos, IncludePosition: p.dmsIncludePos,
TotalIncludes: p.includeCount, TotalIncludes: p.includeCount,
BindsAfterDMS: p.bindsAfterDMS, BindsAfterDMS: p.bindsAfterDMS,
ConfigFormat: p.configFormat,
ReadOnly: p.readOnly,
} }
switch { switch {
@@ -398,6 +404,13 @@ func (p *HyprlandParser) ParseWithDMS() (*HyprlandSection, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
if strings.EqualFold(filepath.Ext(mainConfig), ".lua") {
p.configFormat = "lua"
p.readOnly = false
} else {
p.configFormat = "hyprlang"
p.readOnly = true
}
section, err := p.parseFileWithSource(mainConfig, "") section, err := p.parseFileWithSource(mainConfig, "")
if err != nil { if err != nil {
return nil, err return nil, err
@@ -1004,7 +1017,18 @@ func luaExprToDispatcherParams(expr string) (dispatcher, params string) {
} }
} }
return "exec", strings.TrimSpace(strings.TrimPrefix(expr, "hl.dsp.exec_cmd")) return "exec", strings.TrimSpace(strings.TrimPrefix(expr, "hl.dsp.exec_cmd"))
case strings.Contains(expr, "hl.dsp.window.kill()"): case strings.HasPrefix(expr, "hl.dsp.window.close("):
if arg := luaCallStringArgValue(expr, "hl.dsp.window.close"); arg != "" {
return "closewindow", arg
}
return "closewindow", ""
case strings.HasPrefix(expr, "hl.dsp.window.kill("):
if luaTableBoolFieldValue(expr, "force") {
return "forcekillactive", ""
}
if arg := luaCallStringArgValue(expr, "hl.dsp.window.kill"); arg != "" {
return "killwindow", arg
}
return "killactive", "" return "killactive", ""
case strings.HasPrefix(expr, "hl.dsp.window.fullscreen("): case strings.HasPrefix(expr, "hl.dsp.window.fullscreen("):
switch luaTableStringField(expr, "mode") { switch luaTableStringField(expr, "mode") {
@@ -1014,8 +1038,26 @@ func luaExprToDispatcherParams(expr string) (dispatcher, params string) {
return "fullscreen", "0" return "fullscreen", "0"
} }
return "fullscreen", luaTableStringField(expr, "mode") return "fullscreen", luaTableStringField(expr, "mode")
case strings.HasPrefix(expr, "hl.dsp.window.fullscreen_state("):
internal := luaStringValue(luaTableScalarField(expr, "internal"))
client := luaStringValue(luaTableScalarField(expr, "client"))
return joinDispatcherParams("fullscreenstate", internal, client)
case strings.HasPrefix(expr, "hl.dsp.window.float("): case strings.HasPrefix(expr, "hl.dsp.window.float("):
switch luaTableStringField(expr, "action") {
case "set":
return "setfloating", ""
case "unset":
return "settiled", ""
default:
return "togglefloating", "" return "togglefloating", ""
}
case strings.HasPrefix(expr, "hl.dsp.window.pin("):
if action := luaTableStringField(expr, "action"); action != "" && action != "toggle" {
return "pin", action
}
return "pin", ""
case strings.Contains(expr, "hl.dsp.window.center()"):
return "centerwindow", ""
case strings.Contains(expr, "hl.dsp.group.toggle()"): case strings.Contains(expr, "hl.dsp.group.toggle()"):
return "togglegroup", "" return "togglegroup", ""
case strings.HasPrefix(expr, "hl.dsp.focus("): case strings.HasPrefix(expr, "hl.dsp.focus("):
@@ -1025,18 +1067,43 @@ func luaExprToDispatcherParams(expr string) (dispatcher, params string) {
case luaTableStringField(expr, "monitor") != "": case luaTableStringField(expr, "monitor") != "":
return "focusmonitor", luaTableStringField(expr, "monitor") return "focusmonitor", luaTableStringField(expr, "monitor")
case luaTableStringField(expr, "workspace") != "": case luaTableStringField(expr, "workspace") != "":
if luaTableBoolFieldValue(expr, "on_current_monitor") {
return "focusworkspaceoncurrentmonitor", luaTableStringField(expr, "workspace")
}
return "workspace", luaTableStringField(expr, "workspace") return "workspace", luaTableStringField(expr, "workspace")
case luaTableStringField(expr, "window") != "": case luaTableStringField(expr, "window") != "":
return "focuswindow", luaTableStringField(expr, "window") return "focuswindow", luaTableStringField(expr, "window")
case luaTableBoolFieldValue(expr, "urgent_or_last"):
return "focusurgentorlast", ""
case luaTableBoolFieldValue(expr, "last"):
return "focuscurrentorlast", ""
} }
case strings.HasPrefix(expr, "hl.dsp.window.move("): case strings.HasPrefix(expr, "hl.dsp.window.move("):
switch { switch {
case luaTableScalarField(expr, "x") != "" || luaTableScalarField(expr, "y") != "":
x := luaStringValue(luaTableScalarField(expr, "x"))
y := luaStringValue(luaTableScalarField(expr, "y"))
if x == "" {
x = "0"
}
if y == "" {
y = "0"
}
prefix := ""
if raw, ok := luaTableBoolField(expr, "relative"); ok && !raw {
prefix = "exact "
}
return "moveactive", prefix + x + " " + y
case luaTableStringField(expr, "direction") != "": case luaTableStringField(expr, "direction") != "":
return "movewindow", luaTableStringField(expr, "direction") return "movewindow", luaTableStringField(expr, "direction")
case luaTableStringField(expr, "monitor") != "": case luaTableStringField(expr, "monitor") != "":
return "movewindow", "mon:" + luaTableStringField(expr, "monitor") return "movewindow", "mon:" + luaTableStringField(expr, "monitor")
case luaTableStringField(expr, "workspace") != "": case luaTableStringField(expr, "workspace") != "":
return "movetoworkspace", luaTableStringField(expr, "workspace") action := "movetoworkspace"
if follow, ok := luaTableBoolField(expr, "follow"); ok && !follow {
action = "movetoworkspacesilent"
}
return joinDispatcherParams(action, luaTableStringField(expr, "workspace"), luaTableStringField(expr, "window"))
} }
case expr == "hl.dsp.window.drag()": case expr == "hl.dsp.window.drag()":
return "movewindow", "" return "movewindow", ""
@@ -1052,19 +1119,69 @@ func luaExprToDispatcherParams(expr string) (dispatcher, params string) {
if y == "" { if y == "" {
y = "0" y = "0"
} }
return "resizeactive", x + " " + y prefix := ""
if relative, ok := luaTableBoolField(expr, "relative"); ok && !relative {
prefix = "exact "
} }
return "resizeactive", prefix + x + " " + y
}
case strings.HasPrefix(expr, "hl.dsp.window.swap("):
return "swapwindow", luaTableStringField(expr, "direction")
case strings.HasPrefix(expr, "hl.dsp.window.alter_zorder("):
mode := luaTableStringField(expr, "mode")
if mode == "" {
mode = luaTableStringField(expr, "zheight")
}
return joinDispatcherParams("alterzorder", mode, luaTableStringField(expr, "window"))
case strings.HasPrefix(expr, "hl.dsp.window.set_prop("):
prop := luaTableStringField(expr, "prop")
if prop == "" {
prop = luaTableStringField(expr, "property")
}
return joinDispatcherParams("setprop", luaTableStringField(expr, "window"), prop, luaTableStringField(expr, "value"))
case strings.HasPrefix(expr, "hl.dsp.workspace.rename("):
return joinDispatcherParams("renameworkspace", luaTableStringField(expr, "workspace"), luaTableStringField(expr, "name"))
case strings.HasPrefix(expr, "hl.dsp.workspace.move("):
workspace := luaTableStringField(expr, "workspace")
monitor := luaTableStringField(expr, "monitor")
if workspace != "" {
return joinDispatcherParams("moveworkspacetomonitor", workspace, monitor)
}
return "movecurrentworkspacetomonitor", monitor
case strings.HasPrefix(expr, "hl.dsp.workspace.swap_monitors("):
return joinDispatcherParams("swapactiveworkspaces", luaTableStringField(expr, "monitor1"), luaTableStringField(expr, "monitor2"))
case strings.HasPrefix(expr, "hl.dsp.workspace.toggle_special("):
return "togglespecialworkspace", luaCallStringArgValue(expr, "hl.dsp.workspace.toggle_special")
case strings.HasPrefix(expr, "hl.dsp.layout("): case strings.HasPrefix(expr, "hl.dsp.layout("):
arg := extractLuaCallStringArg(expr, "hl.dsp.layout") if arg := luaCallStringArgValue(expr, "hl.dsp.layout"); arg != "" {
if arg != "" { return "layoutmsg", arg
if u, err := strconv.Unquote(arg); err == nil {
return "layoutmsg", u
}
} }
case strings.HasPrefix(expr, "hl.dsp.dpms("): case strings.HasPrefix(expr, "hl.dsp.dpms("):
if action := luaTableStringField(expr, "action"); action != "" { if action := luaTableStringField(expr, "action"); action != "" {
switch action {
case "enable":
return "dpms", "on"
case "disable":
return "dpms", "off"
}
return "dpms", action return "dpms", action
} }
return "dpms", ""
case strings.HasPrefix(expr, "hl.dsp.submap("):
return "submap", luaCallStringArgValue(expr, "hl.dsp.submap")
case strings.HasPrefix(expr, "hl.dsp.global("):
return "global", luaCallStringArgValue(expr, "hl.dsp.global")
case strings.HasPrefix(expr, "hl.dsp.event("):
return "event", luaCallStringArgValue(expr, "hl.dsp.event")
case strings.HasPrefix(expr, "hl.dsp.pass("):
if window := luaTableStringField(expr, "window"); window != "" {
return "pass", window
}
return "pass", luaCallStringArgValue(expr, "hl.dsp.pass")
case strings.HasPrefix(expr, "hl.dsp.send_shortcut("):
return joinDispatcherParams("sendshortcut", luaTableModsField(expr), luaTableStringField(expr, "key"), luaTableStringField(expr, "window"))
case strings.HasPrefix(expr, "hl.dsp.send_key_state("):
return joinDispatcherParams("sendkeystate", luaTableModsField(expr), luaTableStringField(expr, "key"), luaTableStringField(expr, "state"), luaTableStringField(expr, "window"))
case strings.Contains(expr, "hl.dsp.exit()"): case strings.Contains(expr, "hl.dsp.exit()"):
return "exit", "" return "exit", ""
default: default:
@@ -1073,6 +1190,17 @@ func luaExprToDispatcherParams(expr string) (dispatcher, params string) {
return "exec", "hyprctl dispatch lua:" + expr return "exec", "hyprctl dispatch lua:" + expr
} }
func joinDispatcherParams(dispatcher string, values ...string) (string, string) {
parts := make([]string, 0, len(values))
for _, value := range values {
value = strings.TrimSpace(value)
if value != "" {
parts = append(parts, value)
}
}
return dispatcher, strings.Join(parts, " ")
}
func extractLuaCallStringArg(callExpr, funcName string) string { func extractLuaCallStringArg(callExpr, funcName string) string {
callExpr = strings.TrimSpace(callExpr) callExpr = strings.TrimSpace(callExpr)
prefix := funcName + "(" prefix := funcName + "("
@@ -1100,10 +1228,46 @@ func extractLuaCallStringArg(callExpr, funcName string) string {
return "" return ""
} }
func luaCallStringArgValue(callExpr, funcName string) string {
arg := extractLuaCallStringArg(callExpr, funcName)
if arg == "" {
return ""
}
u, err := strconv.Unquote(arg)
if err != nil {
return ""
}
return u
}
func luaTableStringField(expr, field string) string { func luaTableStringField(expr, field string) string {
return luaStringValue(luaTableScalarField(expr, field)) return luaStringValue(luaTableScalarField(expr, field))
} }
func luaTableModsField(expr string) string {
if mods := luaTableStringField(expr, "mods"); mods != "" {
return mods
}
return luaTableStringField(expr, "mod")
}
func luaTableBoolFieldValue(expr, field string) bool {
value, ok := luaTableBoolField(expr, field)
return ok && value
}
func luaTableBoolField(expr, field string) (bool, bool) {
raw := strings.ToLower(luaTableScalarField(expr, field))
switch raw {
case "true":
return true, true
case "false":
return false, true
default:
return false, false
}
}
func luaTableScalarField(expr, field string) string { func luaTableScalarField(expr, field string) string {
re := regexp.MustCompile(`(?s)\b` + regexp.QuoteMeta(field) + `\s*=\s*("(?:\\.|[^"])*"|'(?:\\.|[^'])*'|\[\[.*?\]\]|-?\d+(?:\.\d+)?|true|false)`) re := regexp.MustCompile(`(?s)\b` + regexp.QuoteMeta(field) + `\s*=\s*("(?:\\.|[^"])*"|'(?:\\.|[^'])*'|\[\[.*?\]\]|-?\d+(?:\.\d+)?|true|false)`)
m := re.FindStringSubmatch(expr) m := re.FindStringSubmatch(expr)
@@ -70,12 +70,17 @@ func TestHyprlandLuaBindRoundTripHelpers(t *testing.T) {
wantParams string wantParams string
}{ }{
{`hl.dsp.exec_cmd([[dms ipc call brightness increment 5 ""]])`, "exec", `dms ipc call brightness increment 5 ""`}, {`hl.dsp.exec_cmd([[dms ipc call brightness increment 5 ""]])`, "exec", `dms ipc call brightness increment 5 ""`},
{`hl.dsp.exec_cmd([[hyprctl dispatch workspace 1]])`, "workspace", "1"},
{`hl.dsp.window.fullscreen({ mode = "maximized", action = "toggle" })`, "fullscreen", "1"}, {`hl.dsp.window.fullscreen({ mode = "maximized", action = "toggle" })`, "fullscreen", "1"},
{`hl.dsp.focus({ workspace = "e+1" })`, "workspace", "e+1"}, {`hl.dsp.focus({ workspace = "e+1" })`, "workspace", "e+1"},
{`hl.dsp.focus({ workspace = "2", on_current_monitor = true })`, "focusworkspaceoncurrentmonitor", "2"},
{`hl.dsp.window.move({ monitor = "l" })`, "movewindow", "mon:l"}, {`hl.dsp.window.move({ monitor = "l" })`, "movewindow", "mon:l"},
{`hl.dsp.window.resize({ x = "-10%", y = 0, relative = true })`, "resizeactive", "-10% 0"}, {`hl.dsp.window.move({ workspace = "special:magic", follow = false })`, "movetoworkspacesilent", "special:magic"},
{`hl.dsp.window.resize({ x = -100, y = 0, relative = true })`, "resizeactive", "-100 0"},
{`hl.dsp.window.resize({ x = 1280, y = 720, relative = false })`, "resizeactive", "exact 1280 720"},
{`hl.dsp.layout("togglesplit")`, "layoutmsg", "togglesplit"}, {`hl.dsp.layout("togglesplit")`, "layoutmsg", "togglesplit"},
{`hl.dsp.dpms({ action = "toggle" })`, "dpms", "toggle"}, {`hl.dsp.dpms({ action = "toggle" })`, "dpms", "toggle"},
{`hl.dsp.workspace.rename({ workspace = "1", name = "work" })`, "renameworkspace", "1 work"},
} }
for _, tt := range tests { for _, tt := range tests {
@@ -119,6 +124,41 @@ hl.bind("SUPER + N", hl.dsp.exec_cmd("dms ipc call notepad toggle")) -- Notepad:
} }
} }
func TestLuaActionStringFromHyprlangActionUsesNativeDispatchers(t *testing.T) {
tests := []struct {
action string
want string
}{
{"workspace 1", `hl.dsp.focus({ workspace = "1" })`},
{"movetoworkspace 2", `hl.dsp.window.move({ workspace = "2" })`},
{"movetoworkspacesilent special:magic", `hl.dsp.window.move({ workspace = "special:magic", follow = false })`},
{"focusmonitor DP-1", `hl.dsp.focus({ monitor = "DP-1" })`},
{"resizeactive exact 1280 720", `hl.dsp.window.resize({ x = 1280, y = 720, relative = false })`},
{"dpms toggle", `hl.dsp.dpms({ action = "toggle" })`},
{"renameworkspace 1 work", `hl.dsp.workspace.rename({ workspace = "1", name = "work" })`},
}
for _, tt := range tests {
t.Run(tt.action, func(t *testing.T) {
got := luaActionStringFromHyprlangAction(tt.action)
if got != tt.want {
t.Fatalf("luaActionStringFromHyprlangAction(%q) = %q, want %q", tt.action, got, tt.want)
}
if strings.Contains(got, "hyprctl dispatch") {
t.Fatalf("expected native Lua dispatcher, got legacy dispatch wrapper: %q", got)
}
})
}
}
func TestLuaActionStringFallsBackForUnsupportedResizePercentages(t *testing.T) {
got := luaActionStringFromHyprlangAction("resizeactive exact 100% 100%")
want := `hl.dsp.exec_cmd("hyprctl dispatch resizeactive exact 100% 100%")`
if got != want {
t.Fatalf("luaActionStringFromHyprlangAction() = %q, want %q", got, want)
}
}
func TestHyprlandLuaBindsUserOverridesDefaults(t *testing.T) { func TestHyprlandLuaBindsUserOverridesDefaults(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
dmsDir := filepath.Join(tmpDir, "dms") dmsDir := filepath.Join(tmpDir, "dms")
@@ -283,6 +323,64 @@ func TestHyprlandRemoveBindWritesNegativeOverrideForDefault(t *testing.T) {
} }
} }
func TestHyprlandSetBindLeavesConfOnlyInstallReadOnly(t *testing.T) {
tmpDir := t.TempDir()
if err := os.WriteFile(filepath.Join(tmpDir, "hyprland.conf"), []byte("bind = SUPER, T, exec, kitty\n"), 0o644); err != nil {
t.Fatal(err)
}
provider := NewHyprlandProvider(tmpDir)
err := provider.SetBind("SUPER+N", "workspace 1", "Workspace 1", nil)
if err == nil {
t.Fatal("expected SetBind to reject conf-only Hyprland config")
}
if !strings.Contains(err.Error(), "read-only") {
t.Fatalf("expected read-only error, got %v", err)
}
if _, err := os.Stat(filepath.Join(tmpDir, "dms", "binds-user.lua")); !os.IsNotExist(err) {
t.Fatalf("expected no Lua override to be created for conf-only config, stat err=%v", err)
}
}
func TestHyprlandSetBindUpdatesSpacedLuaOverrideWithoutDuplicates(t *testing.T) {
tmpDir := t.TempDir()
dmsDir := filepath.Join(tmpDir, "dms")
if err := os.MkdirAll(dmsDir, 0o755); err != nil {
t.Fatal(err)
}
override := `-- DMS user keybind overrides
hl.unbind("SUPER + SHIFT + S")
hl.bind("SUPER + 1", hl.dsp.exec_cmd("hyprctl dispatch workspace 1"))
`
if err := os.WriteFile(filepath.Join(dmsDir, "binds-user.lua"), []byte(override), 0o644); err != nil {
t.Fatal(err)
}
provider := NewHyprlandProvider(tmpDir)
if err := provider.SetBind("SUPER + 1", "workspace 1", "", nil); err != nil {
t.Fatal(err)
}
data, err := os.ReadFile(filepath.Join(dmsDir, "binds-user.lua"))
if err != nil {
t.Fatal(err)
}
got := string(data)
if strings.Count(got, `hl.unbind("SUPER + 1")`) != 1 {
t.Fatalf("expected one SUPER+1 unbind, got:\n%s", got)
}
if strings.Count(got, `hl.bind("SUPER + 1", hl.dsp.focus({ workspace = "1" }))`) != 1 {
t.Fatalf("expected one native SUPER+1 bind, got:\n%s", got)
}
if strings.Contains(got, "hyprctl dispatch workspace 1") {
t.Fatalf("expected old hyprctl workspace dispatcher to be replaced, got:\n%s", got)
}
if !strings.Contains(got, `hl.unbind("SUPER + SHIFT + S")`) {
t.Fatalf("expected unrelated override to be preserved, got:\n%s", got)
}
}
func TestHyprlandRemoveBindReplacesExistingOverrideWithNegativeOverride(t *testing.T) { func TestHyprlandRemoveBindReplacesExistingOverrideWithNegativeOverride(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
dmsDir := filepath.Join(tmpDir, "dms") dmsDir := filepath.Join(tmpDir, "dms")
+2
View File
@@ -25,6 +25,8 @@ type DMSBindsStatus struct {
Effective bool `json:"effective"` Effective bool `json:"effective"`
OverriddenBy int `json:"overriddenBy"` OverriddenBy int `json:"overriddenBy"`
StatusMessage string `json:"statusMessage"` StatusMessage string `json:"statusMessage"`
ConfigFormat string `json:"configFormat,omitempty"`
ReadOnly bool `json:"readOnly,omitempty"`
} }
type CheatSheet struct { type CheatSheet struct {
@@ -44,6 +44,8 @@ type HyprlandRulesParser struct {
dmsIncludePos int dmsIncludePos int
rulesAfterDMS int rulesAfterDMS int
dmsProcessed bool dmsProcessed bool
configFormat string
readOnly bool
requireLineInMain int // hyprland.lua line (1-based) where require("dms.windowrules") occurs; else -1 requireLineInMain int // hyprland.lua line (1-based) where require("dms.windowrules") occurs; else -1
primaryHyprLua string // absolute path to ~/.config/hypr/hyprland.lua when that is the main config primaryHyprLua string // absolute path to ~/.config/hypr/hyprland.lua when that is the main config
@@ -82,10 +84,15 @@ func (p *HyprlandRulesParser) Parse() ([]HyprlandWindowRule, error) {
} }
if strings.EqualFold(filepath.Ext(mainConfig), ".lua") { if strings.EqualFold(filepath.Ext(mainConfig), ".lua") {
p.configFormat = "lua"
p.readOnly = false
p.probeRequireWindowrulesLine(mainConfig) p.probeRequireWindowrulesLine(mainConfig)
if ap, err := filepath.Abs(mainConfig); err == nil { if ap, err := filepath.Abs(mainConfig); err == nil {
p.primaryHyprLua = ap p.primaryHyprLua = ap
} }
} else {
p.configFormat = "hyprlang"
p.readOnly = true
} }
if err := p.parseFile(mainConfig); err != nil { if err := p.parseFile(mainConfig); err != nil {
@@ -300,6 +307,8 @@ func (p *HyprlandRulesParser) buildDMSStatus() *windowrules.DMSRulesStatus {
IncludePosition: p.dmsIncludePos, IncludePosition: p.dmsIncludePos,
TotalIncludes: p.includeCount, TotalIncludes: p.includeCount,
RulesAfterDMS: p.rulesAfterDMS, RulesAfterDMS: p.rulesAfterDMS,
ConfigFormat: p.configFormat,
ReadOnly: p.readOnly,
} }
switch { switch {
@@ -451,6 +460,9 @@ func (p *HyprlandWritableProvider) GetRuleSet() (*windowrules.RuleSet, error) {
} }
func (p *HyprlandWritableProvider) SetRule(rule windowrules.WindowRule) error { func (p *HyprlandWritableProvider) SetRule(rule windowrules.WindowRule) error {
if err := p.ensureWritableConfig(); err != nil {
return err
}
rules, err := p.LoadDMSRules() rules, err := p.LoadDMSRules()
if err != nil { if err != nil {
rules = []windowrules.WindowRule{} rules = []windowrules.WindowRule{}
@@ -472,6 +484,9 @@ func (p *HyprlandWritableProvider) SetRule(rule windowrules.WindowRule) error {
} }
func (p *HyprlandWritableProvider) RemoveRule(id string) error { func (p *HyprlandWritableProvider) RemoveRule(id string) error {
if err := p.ensureWritableConfig(); err != nil {
return err
}
rules, err := p.LoadDMSRules() rules, err := p.LoadDMSRules()
if err != nil { if err != nil {
return err return err
@@ -488,6 +503,9 @@ func (p *HyprlandWritableProvider) RemoveRule(id string) error {
} }
func (p *HyprlandWritableProvider) ReorderRules(ids []string) error { func (p *HyprlandWritableProvider) ReorderRules(ids []string) error {
if err := p.ensureWritableConfig(); err != nil {
return err
}
rules, err := p.LoadDMSRules() rules, err := p.LoadDMSRules()
if err != nil { if err != nil {
return err return err
@@ -513,6 +531,29 @@ func (p *HyprlandWritableProvider) ReorderRules(ids []string) error {
return p.writeDMSRules(newRules) return p.writeDMSRules(newRules)
} }
func (p *HyprlandWritableProvider) ensureWritableConfig() error {
if p.isLegacyConfigReadOnly() {
return fmt.Errorf("hyprland legacy conf configs are read-only; run dms setup to migrate to Lua before editing window rules")
}
return nil
}
func (p *HyprlandWritableProvider) isLegacyConfigReadOnly() bool {
expanded, err := utils.ExpandPath(p.configDir)
if err != nil {
expanded = p.configDir
}
luaPath := filepath.Join(expanded, "hyprland.lua")
if st, err := os.Stat(luaPath); err == nil && st.Mode().IsRegular() {
return false
}
confPath := filepath.Join(expanded, "hyprland.conf")
if st, err := os.Stat(confPath); err == nil && st.Mode().IsRegular() {
return true
}
return false
}
var dmsRuleCommentRegex = regexp.MustCompile(`^#\s*DMS-RULE:\s*id=([^,]+),\s*name=(.*)$`) var dmsRuleCommentRegex = regexp.MustCompile(`^#\s*DMS-RULE:\s*id=([^,]+),\s*name=(.*)$`)
var dmsRuleLuaHDRRegex = regexp.MustCompile(`^\s*--\s*DMS-RULE:\s*id=([^,]+),\s*name=(.*)$`) var dmsRuleLuaHDRRegex = regexp.MustCompile(`^\s*--\s*DMS-RULE:\s*id=([^,]+),\s*name=(.*)$`)
@@ -188,6 +188,27 @@ func TestHyprlandSetAndLoadDMSRules(t *testing.T) {
} }
} }
func TestHyprlandSetRuleLeavesConfOnlyInstallReadOnly(t *testing.T) {
tmpDir := t.TempDir()
if err := os.WriteFile(filepath.Join(tmpDir, "hyprland.conf"), []byte("windowrulev2 = float, class:^(kitty)$\n"), 0o644); err != nil {
t.Fatal(err)
}
provider := NewHyprlandWritableProvider(tmpDir)
rule := newTestWindowRule("test_id", "Test Rule", "^(firefox)$")
rule.Actions.OpenFloating = boolPtr(true)
err := provider.SetRule(rule)
if err == nil {
t.Fatal("expected SetRule to reject conf-only Hyprland config")
}
if !strings.Contains(err.Error(), "read-only") {
t.Fatalf("expected read-only error, got %v", err)
}
if _, err := os.Stat(filepath.Join(tmpDir, "dms", "windowrules.lua")); !os.IsNotExist(err) {
t.Fatalf("expected no Lua windowrules file to be created for conf-only config, stat err=%v", err)
}
}
func TestHyprlandRemoveRule(t *testing.T) { func TestHyprlandRemoveRule(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
provider := NewHyprlandWritableProvider(tmpDir) provider := NewHyprlandWritableProvider(tmpDir)
+2
View File
@@ -79,6 +79,8 @@ type DMSRulesStatus struct {
Effective bool `json:"effective"` Effective bool `json:"effective"`
OverriddenBy int `json:"overriddenBy"` OverriddenBy int `json:"overriddenBy"`
StatusMessage string `json:"statusMessage"` StatusMessage string `json:"statusMessage"`
ConfigFormat string `json:"configFormat,omitempty"`
ReadOnly bool `json:"readOnly,omitempty"`
} }
type RuleSet struct { type RuleSet struct {
@@ -21,8 +21,11 @@ Singleton {
property var includeStatus: ({ property var includeStatus: ({
"exists": false, "exists": false,
"included": false "included": false,
"configFormat": "",
"readOnly": false
}) })
readonly property bool readOnly: CompositorService.isHyprland && includeStatus.readOnly === true
property bool checkingInclude: false property bool checkingInclude: false
property bool fixingInclude: false property bool fixingInclude: false
@@ -481,6 +484,15 @@ Singleton {
// Write compositor config from a neutral config entry and optionally reload // Write compositor config from a neutral config entry and optionally reload
function applyConfigEntry(configEntry, configId, profileName, isManual) { function applyConfigEntry(configEntry, configId, profileName, isManual) {
if (CompositorService.isHyprland && readOnly) {
if (isManual) {
profilesLoading = false;
manualActivation = false;
profileError(I18n.tr("Hyprland conf mode is read-only in Settings"));
}
showHyprlandReadOnlyWarning();
return;
}
ensureEnabledOutput(configEntry); ensureEnabledOutput(configEntry);
// Capture the entry being applied so disabled-output settings fields can read // Capture the entry being applied so disabled-output settings fields can read
// scale/position/transform back even when wlr reports no logical viewport. // scale/position/transform back even when wlr reports no logical viewport.
@@ -1372,7 +1384,9 @@ Singleton {
if (compositor !== "niri" && compositor !== "hyprland" && compositor !== "dwl") { if (compositor !== "niri" && compositor !== "hyprland" && compositor !== "dwl") {
includeStatus = { includeStatus = {
"exists": false, "exists": false,
"included": false "included": false,
"configFormat": "",
"readOnly": false
}; };
return; return;
} }
@@ -1386,7 +1400,9 @@ Singleton {
if (exitCode !== 0) { if (exitCode !== 0) {
includeStatus = { includeStatus = {
"exists": false, "exists": false,
"included": false "included": false,
"configFormat": "",
"readOnly": false
}; };
return; return;
} }
@@ -1395,13 +1411,19 @@ Singleton {
} catch (e) { } catch (e) {
includeStatus = { includeStatus = {
"exists": false, "exists": false,
"included": false "included": false,
"configFormat": "",
"readOnly": false
}; };
} }
}); });
} }
function fixOutputsInclude() { function fixOutputsInclude() {
if (readOnly) {
showHyprlandReadOnlyWarning();
return;
}
const paths = getConfigPaths(); const paths = getConfigPaths();
if (!paths) if (!paths)
return; return;
@@ -1426,6 +1448,10 @@ Singleton {
}); });
} }
function showHyprlandReadOnlyWarning() {
ToastService.showWarning(I18n.tr("Hyprland conf mode"), I18n.tr("This install is still using hyprland.conf. Run dms setup to migrate before editing display settings."), "dms setup", "display-config");
}
function buildOutputsMap() { function buildOutputsMap() {
const map = {}; const map = {};
for (const output of wlrOutputs) { for (const output of wlrOutputs) {
@@ -1514,6 +1540,10 @@ Singleton {
NiriService.generateOutputsConfig(outputsData); NiriService.generateOutputsConfig(outputsData);
break; break;
case "hyprland": case "hyprland":
if (readOnly) {
showHyprlandReadOnlyWarning();
return false;
}
HyprlandService.generateOutputsConfig(outputsData, buildMergedHyprlandSettings()); HyprlandService.generateOutputsConfig(outputsData, buildMergedHyprlandSettings());
break; break;
case "dwl": case "dwl":
@@ -1523,6 +1553,7 @@ Singleton {
WlrOutputService.applyOutputsConfig(outputsData, outputs); WlrOutputService.applyOutputsConfig(outputsData, outputs);
break; break;
} }
return true;
} }
function normalizeOutputPositions(outputsData) { function normalizeOutputPositions(outputsData) {
@@ -1830,6 +1861,10 @@ Singleton {
function applyChanges() { function applyChanges() {
if (!hasPendingChanges) if (!hasPendingChanges)
return; return;
if (CompositorService.isHyprland && readOnly) {
showHyprlandReadOnlyWarning();
return;
}
const changeDescriptions = []; const changeDescriptions = [];
if (formatChanged) { if (formatChanged) {
@@ -12,13 +12,14 @@ StyledRect {
height: warningContent.implicitHeight + Theme.spacingL * 2 height: warningContent.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius radius: Theme.cornerRadius
readonly property bool showError: DisplayConfigState.includeStatus.exists && !DisplayConfigState.includeStatus.included readonly property bool showLegacy: DisplayConfigState.readOnly
readonly property bool showSetup: !DisplayConfigState.includeStatus.exists && !DisplayConfigState.includeStatus.included readonly property bool showError: !showLegacy && DisplayConfigState.includeStatus.exists && !DisplayConfigState.includeStatus.included
readonly property bool showSetup: !showLegacy && !DisplayConfigState.includeStatus.exists && !DisplayConfigState.includeStatus.included
color: (showError || showSetup) ? Theme.withAlpha(Theme.primary, 0.15) : "transparent" color: (showLegacy || showError || showSetup) ? Theme.withAlpha(Theme.primary, 0.15) : "transparent"
border.color: (showError || showSetup) ? Theme.withAlpha(Theme.primary, 0.3) : "transparent" border.color: (showLegacy || showError || showSetup) ? Theme.withAlpha(Theme.primary, 0.3) : "transparent"
border.width: 1 border.width: 1
visible: (showError || showSetup) && DisplayConfigState.hasOutputBackend && !DisplayConfigState.checkingInclude visible: (showLegacy || showError || showSetup) && DisplayConfigState.hasOutputBackend && !DisplayConfigState.checkingInclude
Column { Column {
id: warningContent id: warningContent
@@ -44,6 +45,8 @@ StyledRect {
StyledText { StyledText {
text: { text: {
if (root.showLegacy)
return I18n.tr("Hyprland conf mode");
if (root.showSetup) if (root.showSetup)
return I18n.tr("First Time Setup"); return I18n.tr("First Time Setup");
if (root.showError) if (root.showError)
@@ -59,6 +62,8 @@ StyledRect {
StyledText { StyledText {
text: { text: {
if (root.showLegacy)
return I18n.tr("This install is still using hyprland.conf. Run dms setup to migrate before editing display settings.");
if (root.showSetup) if (root.showSetup)
return I18n.tr("Click 'Setup' to create the outputs config and add include to your compositor config."); return I18n.tr("Click 'Setup' to create the outputs config and add include to your compositor config.");
if (root.showError) if (root.showError)
@@ -75,7 +80,7 @@ StyledRect {
DankButton { DankButton {
id: fixButton id: fixButton
visible: root.showError || root.showSetup visible: !root.showLegacy && (root.showError || root.showSetup)
text: { text: {
if (DisplayConfigState.fixingInclude) if (DisplayConfigState.fixingInclude)
return I18n.tr("Fixing..."); return I18n.tr("Fixing...");
+20 -9
View File
@@ -84,6 +84,10 @@ Item {
} }
function startNewBind() { function startNewBind() {
if (KeybindsService.readOnly) {
KeybindsService.showHyprlandReadOnlyWarning();
return;
}
showingNewBind = true; showingNewBind = true;
expandedKey = ""; expandedKey = "";
} }
@@ -292,7 +296,7 @@ Item {
StyledText { StyledText {
readonly property string bindsFile: KeybindsService.currentProvider === "niri" ? "dms/binds.kdl" : KeybindsService.currentProvider === "hyprland" ? "dms/binds-user.lua" : "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) text: KeybindsService.readOnly ? I18n.tr("Hyprland conf mode is read-only in Settings") : I18n.tr("Click any shortcut to edit. Changes save to %1").arg(bindsFile)
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
@@ -326,7 +330,7 @@ Item {
iconSize: Theme.iconSize iconSize: Theme.iconSize
iconColor: Theme.primary iconColor: Theme.primary
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
enabled: !keybindsTab.showingNewBind enabled: !keybindsTab.showingNewBind && !KeybindsService.readOnly
opacity: enabled ? 1 : 0.5 opacity: enabled ? 1 : 0.5
onClicked: keybindsTab.startNewBind() onClicked: keybindsTab.startNewBind()
} }
@@ -342,14 +346,15 @@ Item {
radius: Theme.cornerRadius radius: Theme.cornerRadius
readonly property var status: KeybindsService.dmsStatus readonly property var status: KeybindsService.dmsStatus
readonly property bool showError: !status.included && status.exists readonly property bool showLegacy: KeybindsService.readOnly
readonly property bool showWarning: status.included && status.overriddenBy > 0 readonly property bool showError: !showLegacy && !status.included && status.exists
readonly property bool showSetup: !status.exists readonly property bool showWarning: !showLegacy && status.included && status.overriddenBy > 0
readonly property bool showSetup: !showLegacy && !status.exists
color: (showError || showWarning || showSetup) ? Theme.withAlpha(Theme.primary, 0.15) : "transparent" color: (showLegacy || showError || showWarning || showSetup) ? Theme.withAlpha(Theme.primary, 0.15) : "transparent"
border.color: (showError || showWarning || showSetup) ? Theme.withAlpha(Theme.primary, 0.3) : "transparent" border.color: (showLegacy || showError || showWarning || showSetup) ? Theme.withAlpha(Theme.primary, 0.3) : "transparent"
border.width: 1 border.width: 1
visible: (showError || showWarning || showSetup) && !KeybindsService.loading visible: (showLegacy || showError || showWarning || showSetup) && !KeybindsService.loading
Column { Column {
id: warningSection id: warningSection
@@ -375,6 +380,8 @@ Item {
StyledText { StyledText {
text: { text: {
if (warningBox.showLegacy)
return I18n.tr("Hyprland conf mode");
if (warningBox.showSetup) if (warningBox.showSetup)
return I18n.tr("First Time Setup"); return I18n.tr("First Time Setup");
if (warningBox.showError) if (warningBox.showError)
@@ -391,6 +398,8 @@ Item {
StyledText { StyledText {
readonly property string bindsFile: KeybindsService.currentProvider === "niri" ? "dms/binds.kdl" : KeybindsService.currentProvider === "hyprland" ? "dms/binds-user.lua" : "dms/binds.conf" readonly property string bindsFile: KeybindsService.currentProvider === "niri" ? "dms/binds.kdl" : KeybindsService.currentProvider === "hyprland" ? "dms/binds-user.lua" : "dms/binds.conf"
text: { text: {
if (warningBox.showLegacy)
return I18n.tr("This install is still using hyprland.conf. Run dms setup to migrate before editing shortcuts in Settings.");
if (warningBox.showSetup) if (warningBox.showSetup)
return I18n.tr("Click 'Setup' to create %1 and add include to config.").arg(bindsFile); return I18n.tr("Click 'Setup' to create %1 and add include to config.").arg(bindsFile);
if (warningBox.showError) if (warningBox.showError)
@@ -411,7 +420,7 @@ Item {
DankButton { DankButton {
id: fixButton id: fixButton
visible: warningBox.showError || warningBox.showSetup visible: !warningBox.showLegacy && (warningBox.showError || warningBox.showSetup)
text: { text: {
if (KeybindsService.fixing) if (KeybindsService.fixing)
return I18n.tr("Fixing..."); return I18n.tr("Fixing...");
@@ -559,6 +568,7 @@ Item {
desc: "" desc: ""
}) })
panelWindow: keybindsTab.parentModal panelWindow: keybindsTab.parentModal
readOnly: KeybindsService.readOnly
onSaveBind: (originalKey, newData) => keybindsTab.saveNewBind(newData) onSaveBind: (originalKey, newData) => keybindsTab.saveNewBind(newData)
onCancelEdit: keybindsTab.cancelNewBind() onCancelEdit: keybindsTab.cancelNewBind()
} }
@@ -668,6 +678,7 @@ Item {
bindData: modelData bindData: modelData
isExpanded: keybindsTab.expandedKey === modelData.action isExpanded: keybindsTab.expandedKey === modelData.action
panelWindow: keybindsTab.parentModal panelWindow: keybindsTab.parentModal
readOnly: KeybindsService.readOnly
onToggleExpand: keybindsTab.toggleExpanded(modelData.action) onToggleExpand: keybindsTab.toggleExpanded(modelData.action)
onSaveBind: (originalKey, newData) => { onSaveBind: (originalKey, newData) => {
KeybindsService.saveBind(originalKey, newData); KeybindsService.saveBind(originalKey, newData);
+17 -4
View File
@@ -23,8 +23,11 @@ Item {
property var cursorIncludeStatus: ({ property var cursorIncludeStatus: ({
"exists": false, "exists": false,
"included": false "included": false,
"configFormat": "",
"readOnly": false
}) })
readonly property bool cursorReadOnly: CompositorService.isHyprland && cursorIncludeStatus.readOnly === true
property bool checkingCursorInclude: false property bool checkingCursorInclude: false
property bool fixingCursorInclude: false property bool fixingCursorInclude: false
@@ -62,7 +65,9 @@ Item {
if (compositor !== "niri" && compositor !== "hyprland" && compositor !== "dwl") { if (compositor !== "niri" && compositor !== "hyprland" && compositor !== "dwl") {
cursorIncludeStatus = { cursorIncludeStatus = {
"exists": false, "exists": false,
"included": false "included": false,
"configFormat": "",
"readOnly": false
}; };
return; return;
} }
@@ -76,7 +81,9 @@ Item {
if (exitCode !== 0) { if (exitCode !== 0) {
cursorIncludeStatus = { cursorIncludeStatus = {
"exists": false, "exists": false,
"included": false "included": false,
"configFormat": "",
"readOnly": false
}; };
return; return;
} }
@@ -85,13 +92,19 @@ Item {
} catch (e) { } catch (e) {
cursorIncludeStatus = { cursorIncludeStatus = {
"exists": false, "exists": false,
"included": false "included": false,
"configFormat": "",
"readOnly": false
}; };
} }
}); });
} }
function fixCursorInclude() { function fixCursorInclude() {
if (cursorReadOnly) {
ToastService.showWarning(I18n.tr("Hyprland conf mode"), I18n.tr("This install is still using hyprland.conf. Run dms setup to migrate before editing cursor settings."), "dms setup", "hyprland-migration");
return;
}
const paths = getCursorConfigPaths(); const paths = getCursorConfigPaths();
if (!paths) if (!paths)
return; return;
+59 -17
View File
@@ -19,8 +19,11 @@ Item {
property var parentModal: null property var parentModal: null
property var windowRulesIncludeStatus: ({ property var windowRulesIncludeStatus: ({
"exists": false, "exists": false,
"included": false "included": false,
"configFormat": "",
"readOnly": false
}) })
readonly property bool readOnly: CompositorService.isHyprland && windowRulesIncludeStatus.readOnly === true
property bool checkingInclude: false property bool checkingInclude: false
property bool fixingInclude: false property bool fixingInclude: false
property var windowRules: [] property var windowRules: []
@@ -84,7 +87,9 @@ Item {
if (result.dmsStatus) { if (result.dmsStatus) {
windowRulesIncludeStatus = { windowRulesIncludeStatus = {
"exists": result.dmsStatus.exists, "exists": result.dmsStatus.exists,
"included": result.dmsStatus.included "included": result.dmsStatus.included,
"configFormat": result.dmsStatus.configFormat ?? "",
"readOnly": result.dmsStatus.readOnly === true
}; };
} }
} catch (e) { } catch (e) {
@@ -94,6 +99,10 @@ Item {
} }
function removeRule(ruleId) { function removeRule(ruleId) {
if (readOnly) {
showHyprlandReadOnlyWarning();
return;
}
const compositor = CompositorService.compositor; const compositor = CompositorService.compositor;
if (compositor !== "niri" && compositor !== "hyprland") if (compositor !== "niri" && compositor !== "hyprland")
return; return;
@@ -107,6 +116,10 @@ Item {
} }
function reorderRules(fromIndex, toIndex) { function reorderRules(fromIndex, toIndex) {
if (readOnly) {
showHyprlandReadOnlyWarning();
return;
}
if (fromIndex === toIndex) if (fromIndex === toIndex)
return; return;
@@ -131,7 +144,9 @@ Item {
if (compositor !== "niri" && compositor !== "hyprland") { if (compositor !== "niri" && compositor !== "hyprland") {
windowRulesIncludeStatus = { windowRulesIncludeStatus = {
"exists": false, "exists": false,
"included": false "included": false,
"configFormat": "",
"readOnly": false
}; };
return; return;
} }
@@ -143,7 +158,9 @@ Item {
if (exitCode !== 0) { if (exitCode !== 0) {
windowRulesIncludeStatus = { windowRulesIncludeStatus = {
"exists": false, "exists": false,
"included": false "included": false,
"configFormat": "",
"readOnly": false
}; };
return; return;
} }
@@ -152,13 +169,19 @@ Item {
} catch (e) { } catch (e) {
windowRulesIncludeStatus = { windowRulesIncludeStatus = {
"exists": false, "exists": false,
"included": false "included": false,
"configFormat": "",
"readOnly": false
}; };
} }
}); });
} }
function fixWindowRulesInclude() { function fixWindowRulesInclude() {
if (readOnly) {
showHyprlandReadOnlyWarning();
return;
}
const paths = getWindowRulesConfigPaths(); const paths = getWindowRulesConfigPaths();
if (!paths) if (!paths)
return; return;
@@ -182,6 +205,10 @@ Item {
} }
function openRuleModal(window) { function openRuleModal(window) {
if (readOnly) {
showHyprlandReadOnlyWarning();
return;
}
if (!PopoutService.windowRuleModalLoader) if (!PopoutService.windowRuleModalLoader)
return; return;
PopoutService.windowRuleModalLoader.active = true; PopoutService.windowRuleModalLoader.active = true;
@@ -192,6 +219,10 @@ Item {
} }
function editRule(rule) { function editRule(rule) {
if (readOnly) {
showHyprlandReadOnlyWarning();
return;
}
if (!PopoutService.windowRuleModalLoader) if (!PopoutService.windowRuleModalLoader)
return; return;
PopoutService.windowRuleModalLoader.active = true; PopoutService.windowRuleModalLoader.active = true;
@@ -201,6 +232,10 @@ Item {
} }
} }
function showHyprlandReadOnlyWarning() {
ToastService.showWarning(I18n.tr("Hyprland conf mode"), I18n.tr("This install is still using hyprland.conf. Run dms setup to migrate before editing window rules in Settings."), "dms setup", "hyprland-migration");
}
Component.onCompleted: { Component.onCompleted: {
if (CompositorService.isNiri || CompositorService.isHyprland) { if (CompositorService.isNiri || CompositorService.isHyprland) {
checkWindowRulesIncludeStatus(); checkWindowRulesIncludeStatus();
@@ -274,6 +309,8 @@ Item {
iconName: "add" iconName: "add"
iconSize: Theme.iconSize iconSize: Theme.iconSize
iconColor: Theme.primary iconColor: Theme.primary
enabled: !root.readOnly
opacity: enabled ? 1 : 0.5
onClicked: root.openRuleModal() onClicked: root.openRuleModal()
} }
} }
@@ -322,13 +359,14 @@ Item {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
radius: Theme.cornerRadius radius: Theme.cornerRadius
readonly property bool showError: root.windowRulesIncludeStatus.exists && !root.windowRulesIncludeStatus.included readonly property bool showLegacy: root.readOnly
readonly property bool showSetup: !root.windowRulesIncludeStatus.exists && !root.windowRulesIncludeStatus.included readonly property bool showError: !showLegacy && root.windowRulesIncludeStatus.exists && !root.windowRulesIncludeStatus.included
readonly property bool showSetup: !showLegacy && !root.windowRulesIncludeStatus.exists && !root.windowRulesIncludeStatus.included
color: (showError || showSetup) ? Theme.withAlpha(Theme.warning, 0.15) : "transparent" color: (showLegacy || showError || showSetup) ? Theme.withAlpha(Theme.warning, 0.15) : "transparent"
border.color: (showError || showSetup) ? Theme.withAlpha(Theme.warning, 0.3) : "transparent" border.color: (showLegacy || showError || showSetup) ? Theme.withAlpha(Theme.warning, 0.3) : "transparent"
border.width: 1 border.width: 1
visible: (showError || showSetup) && !root.checkingInclude && (CompositorService.isNiri || CompositorService.isHyprland) visible: (showLegacy || showError || showSetup) && !root.checkingInclude && (CompositorService.isNiri || CompositorService.isHyprland)
Row { Row {
id: warningSection id: warningSection
@@ -349,7 +387,7 @@ Item {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
StyledText { StyledText {
text: warningBox.showSetup ? I18n.tr("Window Rules Not Configured") : I18n.tr("Window Rules Include Missing") text: warningBox.showLegacy ? I18n.tr("Hyprland conf mode") : (warningBox.showSetup ? I18n.tr("Window Rules Not Configured") : I18n.tr("Window Rules Include Missing"))
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium font.weight: Font.Medium
color: Theme.warning color: Theme.warning
@@ -359,7 +397,7 @@ Item {
StyledText { StyledText {
readonly property string rulesFile: CompositorService.isNiri ? "dms/windowrules.kdl" : "dms/windowrules.lua" 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) text: warningBox.showLegacy ? I18n.tr("This install is still using hyprland.conf. Run dms setup to migrate before editing window rules in Settings.") : (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 font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
@@ -370,7 +408,7 @@ Item {
DankButton { DankButton {
id: fixButton id: fixButton
visible: warningBox.showError || warningBox.showSetup visible: !warningBox.showLegacy && (warningBox.showError || warningBox.showSetup)
text: root.fixingInclude ? I18n.tr("Fixing...") : (warningBox.showSetup ? I18n.tr("Setup") : I18n.tr("Fix Now")) text: root.fixingInclude ? I18n.tr("Fixing...") : (warningBox.showSetup ? I18n.tr("Setup") : I18n.tr("Fix Now"))
backgroundColor: Theme.warning backgroundColor: Theme.warning
textColor: Theme.background textColor: Theme.background
@@ -611,6 +649,8 @@ Item {
iconSize: 16 iconSize: 16
backgroundColor: "transparent" backgroundColor: "transparent"
iconColor: Theme.surfaceVariantText iconColor: Theme.surfaceVariantText
enabled: !root.readOnly
opacity: enabled ? 1 : 0.5
onClicked: root.editRule(ruleDelegateItem.liveRuleData) onClicked: root.editRule(ruleDelegateItem.liveRuleData)
} }
@@ -621,12 +661,14 @@ Item {
iconSize: 16 iconSize: 16
backgroundColor: "transparent" backgroundColor: "transparent"
iconColor: deleteArea.containsMouse ? Theme.error : Theme.surfaceVariantText iconColor: deleteArea.containsMouse ? Theme.error : Theme.surfaceVariantText
enabled: !root.readOnly
opacity: enabled ? 1 : 0.5
MouseArea { MouseArea {
id: deleteArea id: deleteArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: !root.readOnly
cursorShape: Qt.PointingHandCursor cursorShape: root.readOnly ? Qt.ArrowCursor : Qt.PointingHandCursor
onClicked: root.removeRule(ruleDelegateItem.ruleIdRef) onClicked: root.removeRule(ruleDelegateItem.ruleIdRef)
} }
} }
@@ -641,8 +683,8 @@ Item {
width: 40 width: 40
height: ruleCard.height height: ruleCard.height
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.SizeVerCursor cursorShape: root.readOnly ? Qt.ArrowCursor : Qt.SizeVerCursor
drag.target: ruleDelegateItem.held ? ruleDelegateItem : undefined drag.target: !root.readOnly && ruleDelegateItem.held ? ruleDelegateItem : undefined
drag.axis: Drag.YAxis drag.axis: Drag.YAxis
preventStealing: true preventStealing: true
+26
View File
@@ -18,9 +18,17 @@ Singleton {
readonly property string layoutPath: hyprDmsDir + "/layout.lua" readonly property string layoutPath: hyprDmsDir + "/layout.lua"
readonly property string cursorPath: hyprDmsDir + "/cursor.lua" readonly property string cursorPath: hyprDmsDir + "/cursor.lua"
readonly property string windowrulesPath: hyprDmsDir + "/windowrules.lua" readonly property string windowrulesPath: hyprDmsDir + "/windowrules.lua"
readonly property bool luaConfigActive: CompositorService.isHyprland && Hyprland.usingLua === true
property int _lastGapValue: -1 property int _lastGapValue: -1
onLuaConfigActiveChanged: {
if (luaConfigActive) {
Qt.callLater(generateLayoutConfig);
Qt.callLater(ensureWindowrulesConfig);
}
}
Component.onCompleted: { Component.onCompleted: {
if (CompositorService.isHyprland) { if (CompositorService.isHyprland) {
Qt.callLater(generateLayoutConfig); Qt.callLater(generateLayoutConfig);
@@ -29,6 +37,8 @@ Singleton {
} }
function ensureWindowrulesConfig() { function ensureWindowrulesConfig() {
if (!canWriteLuaConfig("windowrules"))
return;
Proc.runCommand("hypr-ensure-windowrules", ["sh", "-c", `mkdir -p "${hyprDmsDir}" && [ ! -f "${windowrulesPath}" ] && touch "${windowrulesPath}" || true`], (output, exitCode) => { Proc.runCommand("hypr-ensure-windowrules", ["sh", "-c", `mkdir -p "${hyprDmsDir}" && [ ! -f "${windowrulesPath}" ] && touch "${windowrulesPath}" || true`], (output, exitCode) => {
if (exitCode !== 0) if (exitCode !== 0)
log.warn("Failed to ensure windowrules.lua:", output); log.warn("Failed to ensure windowrules.lua:", output);
@@ -66,6 +76,13 @@ Singleton {
return JSON.stringify(String(str ?? "")); return JSON.stringify(String(str ?? ""));
} }
function canWriteLuaConfig(name) {
if (luaConfigActive)
return true;
log.info("Skipping Hyprland", name || "config", "Lua write because the active Hyprland config is not Lua");
return false;
}
function forceFlagValue(value) { function forceFlagValue(value) {
if (value === true) if (value === true)
return 1; return 1;
@@ -75,6 +92,11 @@ Singleton {
} }
function generateOutputsConfig(outputsData, hyprlandSettings, callback) { function generateOutputsConfig(outputsData, hyprlandSettings, callback) {
if (!canWriteLuaConfig("outputs")) {
if (callback)
callback(false);
return;
}
if (!outputsData || Object.keys(outputsData).length === 0) { if (!outputsData || Object.keys(outputsData).length === 0) {
if (callback) if (callback)
callback(false); callback(false);
@@ -172,6 +194,8 @@ Singleton {
function generateLayoutConfig() { function generateLayoutConfig() {
if (!CompositorService.isHyprland) if (!CompositorService.isHyprland)
return; return;
if (!canWriteLuaConfig("layout"))
return;
const defaultRadius = typeof SettingsData !== "undefined" ? SettingsData.cornerRadius : 12; const defaultRadius = typeof SettingsData !== "undefined" ? SettingsData.cornerRadius : 12;
const defaultGaps = typeof SettingsData !== "undefined" ? Math.max(4, (SettingsData.barConfigs[0]?.spacing ?? 4)) : 4; const defaultGaps = typeof SettingsData !== "undefined" ? Math.max(4, (SettingsData.barConfigs[0]?.spacing ?? 4)) : 4;
@@ -254,6 +278,8 @@ hl.config({
function generateCursorConfig() { function generateCursorConfig() {
if (!CompositorService.isHyprland) if (!CompositorService.isHyprland)
return; return;
if (!canWriteLuaConfig("cursor"))
return;
const settings = typeof SettingsData !== "undefined" ? SettingsData.cursorSettings : null; const settings = typeof SettingsData !== "undefined" ? SettingsData.cursorSettings : null;
if (!settings) { if (!settings) {
+33 -3
View File
@@ -52,7 +52,9 @@ Singleton {
"bindsAfterDms": 0, "bindsAfterDms": 0,
"effective": true, "effective": true,
"overriddenBy": 0, "overriddenBy": 0,
"statusMessage": "" "statusMessage": "",
"configFormat": "",
"readOnly": false
}) })
property var _rawData: null property var _rawData: null
@@ -102,6 +104,7 @@ Singleton {
return ""; return "";
} }
} }
readonly property bool readOnly: currentProvider === "hyprland" && dmsStatus.readOnly === true
readonly property var actionTypes: Actions.getActionTypes() readonly property var actionTypes: Actions.getActionTypes()
readonly property var dmsActions: getDmsActions() readonly property var dmsActions: getDmsActions()
@@ -258,6 +261,10 @@ Singleton {
function fixDmsBindsInclude() { function fixDmsBindsInclude() {
if (fixing || dmsBindsIncluded || !compositorConfigDir) if (fixing || dmsBindsIncluded || !compositorConfigDir)
return; return;
if (readOnly) {
showHyprlandReadOnlyWarning();
return;
}
fixing = true; fixing = true;
const timestamp = Math.floor(Date.now() / 1000); const timestamp = Math.floor(Date.now() / 1000);
const backupPath = `${mainConfigPath}.dmsbackup${timestamp}`; const backupPath = `${mainConfigPath}.dmsbackup${timestamp}`;
@@ -343,7 +350,9 @@ Singleton {
"bindsAfterDms": status.bindsAfterDms ?? 0, "bindsAfterDms": status.bindsAfterDms ?? 0,
"effective": status.effective ?? true, "effective": status.effective ?? true,
"overriddenBy": status.overriddenBy ?? 0, "overriddenBy": status.overriddenBy ?? 0,
"statusMessage": status.statusMessage ?? "" "statusMessage": status.statusMessage ?? "",
"configFormat": status.configFormat ?? "",
"readOnly": status.readOnly === true
}; };
} }
_maybeWarnHyprlandLegacyConf(); _maybeWarnHyprlandLegacyConf();
@@ -482,6 +491,10 @@ Singleton {
} }
function saveBind(originalKey, bindData) { function saveBind(originalKey, bindData) {
if (readOnly) {
showHyprlandReadOnlyWarning();
return;
}
if (!bindData.key || !Actions.isValidAction(bindData.action)) if (!bindData.key || !Actions.isValidAction(bindData.action))
return; return;
saving = true; saving = true;
@@ -510,13 +523,26 @@ Singleton {
return; return;
if (currentProvider !== "hyprland") if (currentProvider !== "hyprland")
return; return;
if (readOnly) {
_hyprlandLegacyWarnShown = true;
showHyprlandReadOnlyWarning();
return;
}
if (!dmsStatus.exists || dmsStatus.included) if (!dmsStatus.exists || dmsStatus.included)
return; return;
_hyprlandLegacyWarnShown = true; _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"); ToastService.showWarning(I18n.tr("Hyprland config include missing"), I18n.tr("DMS Settings writes Lua keybinds. Add the DMS include so edits apply."), "dms setup", "hyprland-migration");
}
function showHyprlandReadOnlyWarning() {
ToastService.showWarning(I18n.tr("Hyprland conf mode"), I18n.tr("This install is still using hyprland.conf. Run dms setup to migrate before editing shortcuts in Settings."), "dms setup", "hyprland-migration");
} }
function removeBind(key) { function removeBind(key) {
if (readOnly) {
showHyprlandReadOnlyWarning();
return;
}
if (!key) if (!key)
return; return;
removeProcess.command = ["dms", "keybinds", "remove", currentProvider, key]; removeProcess.command = ["dms", "keybinds", "remove", currentProvider, key];
@@ -525,6 +551,10 @@ Singleton {
} }
function resetBind(key) { function resetBind(key) {
if (readOnly) {
showHyprlandReadOnlyWarning();
return;
}
if (!key) if (!key)
return; return;
removeProcess.command = ["dms", "keybinds", "reset", currentProvider, key]; removeProcess.command = ["dms", "keybinds", "reset", currentProvider, key];
+37 -8
View File
@@ -21,6 +21,7 @@ Item {
property var panelWindow: null property var panelWindow: null
property bool recording: false property bool recording: false
property bool isNew: false property bool isNew: false
property bool readOnly: false
property string restoreKey: "" property string restoreKey: ""
property int editingKeyIndex: -1 property int editingKeyIndex: -1
@@ -160,6 +161,10 @@ Item {
} }
function startAddingNewKey() { function startAddingNewKey() {
if (readOnly) {
KeybindsService.showHyprlandReadOnlyWarning();
return;
}
addingNewKey = true; addingNewKey = true;
editingKeyIndex = -1; editingKeyIndex = -1;
editKey = ""; editKey = "";
@@ -181,6 +186,8 @@ Item {
} }
function updateEdit(changes) { function updateEdit(changes) {
if (readOnly)
return;
if (changes.key !== undefined) if (changes.key !== undefined)
editKey = changes.key; editKey = changes.key;
if (changes.action !== undefined) if (changes.action !== undefined)
@@ -208,6 +215,8 @@ Item {
} }
function canSave() { function canSave() {
if (readOnly)
return false;
if (!editKey) if (!editKey)
return false; return false;
if (!Actions.isValidAction(editAction)) if (!Actions.isValidAction(editAction))
@@ -216,6 +225,10 @@ Item {
} }
function doSave() { function doSave() {
if (readOnly) {
KeybindsService.showHyprlandReadOnlyWarning();
return;
}
if (!canSave()) if (!canSave())
return; return;
const origKey = addingNewKey ? "" : _originalKey; const origKey = addingNewKey ? "" : _originalKey;
@@ -247,6 +260,10 @@ Item {
} }
function startRecording() { function startRecording() {
if (readOnly) {
KeybindsService.showHyprlandReadOnlyWarning();
return;
}
recording = true; recording = true;
} }
@@ -438,6 +455,7 @@ Item {
anchors.top: parent.top anchors.top: parent.top
anchors.margins: Theme.spacingL anchors.margins: Theme.spacingL
spacing: Theme.spacingM spacing: Theme.spacingM
enabled: !root.readOnly
Rectangle { Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
@@ -554,7 +572,7 @@ Item {
height: root._chipHeight height: root._chipHeight
radius: root._chipHeight / 4 radius: root._chipHeight / 4
color: root.addingNewKey ? Theme.primary : Theme.surfaceVariant color: root.addingNewKey ? Theme.primary : Theme.surfaceVariant
visible: !root.isNew visible: !root.isNew && !root.readOnly
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
@@ -644,6 +662,7 @@ Item {
iconName: root.recording ? "close" : "radio_button_checked" iconName: root.recording ? "close" : "radio_button_checked"
iconSize: Theme.iconSizeSmall iconSize: Theme.iconSizeSmall
iconColor: root.recording ? Theme.error : Theme.primary iconColor: root.recording ? Theme.error : Theme.primary
enabled: !root.readOnly
onClicked: root.recording ? root.stopRecording() : root.startRecording() onClicked: root.recording ? root.stopRecording() : root.startRecording()
} }
} }
@@ -746,7 +765,7 @@ Item {
Layout.preferredHeight: root._inputHeight Layout.preferredHeight: root._inputHeight
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: root.addingNewKey ? Theme.primary : Theme.surfaceVariant color: root.addingNewKey ? Theme.primary : Theme.surfaceVariant
visible: root.keys.length === 1 && !root.isNew visible: root.keys.length === 1 && !root.isNew && !root.readOnly
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
@@ -861,6 +880,8 @@ Item {
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
if (root.readOnly)
return;
switch (typeDelegate.modelData.id) { switch (typeDelegate.modelData.id) {
case "dms": case "dms":
root.updateEdit({ root.updateEdit({
@@ -926,6 +947,8 @@ Item {
enableFuzzySearch: true enableFuzzySearch: true
maxPopupHeight: 300 maxPopupHeight: 300
onValueChanged: value => { onValueChanged: value => {
if (root.readOnly)
return;
const actions = KeybindsService.getDmsActions(); const actions = KeybindsService.getDmsActions();
for (const act of actions) { for (const act of actions) {
if (act.label === value) { if (act.label === value) {
@@ -1176,8 +1199,12 @@ Item {
id: customToggleArea id: customToggleArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: root.readOnly ? Qt.ArrowCursor : Qt.PointingHandCursor
onClicked: root.useCustomCompositor = true onClicked: {
if (root.readOnly)
return;
root.useCustomCompositor = true;
}
} }
} }
} }
@@ -1418,8 +1445,10 @@ Item {
id: presetToggleArea id: presetToggleArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: root.readOnly ? Qt.ArrowCursor : Qt.PointingHandCursor
onClicked: { onClicked: {
if (root.readOnly)
return;
root.useCustomCompositor = false; root.useCustomCompositor = false;
root.updateEdit({ root.updateEdit({
"action": "close-window", "action": "close-window",
@@ -1768,7 +1797,7 @@ Item {
iconName: "delete" iconName: "delete"
iconSize: Theme.iconSize - 4 iconSize: Theme.iconSize - 4
iconColor: Theme.error iconColor: Theme.error
visible: root.editingKeyIndex >= 0 && root.editingKeyIndex < root.keys.length && (root.keys[root.editingKeyIndex].isDMSManaged || 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 && !root.readOnly
onClicked: root.removeBind(root._originalKey) onClicked: root.removeBind(root._originalKey)
} }
@@ -1777,7 +1806,7 @@ Item {
buttonHeight: root._buttonHeight buttonHeight: root._buttonHeight
backgroundColor: Theme.surfaceContainer backgroundColor: Theme.surfaceContainer
textColor: Theme.primary 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 visible: root.editingKeyIndex >= 0 && root.editingKeyIndex < root.keys.length && root.keys[root.editingKeyIndex].isOverride === true && root.keys[root.editingKeyIndex].hasDefault === true && !root.isNew && !root.readOnly
onClicked: root.resetBind(root._originalKey) onClicked: root.resetBind(root._originalKey)
} }
@@ -1786,7 +1815,7 @@ Item {
} }
StyledText { StyledText {
text: !root.canSave() ? I18n.tr("Set key and action to save") : (root.hasChanges ? I18n.tr("Unsaved changes") : I18n.tr("No changes")) text: root.readOnly ? I18n.tr("Read-only legacy config") : (!root.canSave() ? I18n.tr("Set key and action to save") : (root.hasChanges ? I18n.tr("Unsaved changes") : I18n.tr("No changes")))
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: root.hasChanges ? Theme.surfaceText : Theme.surfaceVariantText color: root.hasChanges ? Theme.surfaceText : Theme.surfaceVariantText
visible: !root.isNew visible: !root.isNew