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:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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...");
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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];
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user