mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-06-08 04:09:15 -04:00
feat(Hyprland): Introduce Lua support for Hyprland configurations
- Note: We do not convert your existing conf configs to lua. This update only reflects DMS defaults state - Updated README.md to reflect changes - Updated Keyboard shortcut support
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
|
||||
@@ -48,7 +49,7 @@ func (h *HyprlandProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
|
||||
h.parsed = true
|
||||
|
||||
categorizedBinds := make(map[string][]keybinds.Keybind)
|
||||
h.convertSection(result.Section, "", categorizedBinds, result.ConflictingConfigs)
|
||||
h.convertSection(result.Section, "", categorizedBinds, result.ConflictingConfigs, result.DefaultDMSKeys)
|
||||
|
||||
sheet := &keybinds.CheatSheet{
|
||||
Title: "Hyprland Keybinds",
|
||||
@@ -88,7 +89,7 @@ func (h *HyprlandProvider) HasDMSBindsIncluded() bool {
|
||||
return h.dmsBindsIncluded
|
||||
}
|
||||
|
||||
func (h *HyprlandProvider) convertSection(section *HyprlandSection, subcategory string, categorizedBinds map[string][]keybinds.Keybind, conflicts map[string]*HyprlandKeyBinding) {
|
||||
func (h *HyprlandProvider) convertSection(section *HyprlandSection, subcategory string, categorizedBinds map[string][]keybinds.Keybind, conflicts map[string]*HyprlandKeyBinding, defaultKeys map[string]bool) {
|
||||
currentSubcat := subcategory
|
||||
if section.Name != "" {
|
||||
currentSubcat = section.Name
|
||||
@@ -96,12 +97,12 @@ func (h *HyprlandProvider) convertSection(section *HyprlandSection, subcategory
|
||||
|
||||
for _, kb := range section.Keybinds {
|
||||
category := h.categorizeByDispatcher(kb.Dispatcher)
|
||||
bind := h.convertKeybind(&kb, currentSubcat, conflicts)
|
||||
bind := h.convertKeybind(&kb, currentSubcat, conflicts, defaultKeys)
|
||||
categorizedBinds[category] = append(categorizedBinds[category], bind)
|
||||
}
|
||||
|
||||
for _, child := range section.Children {
|
||||
h.convertSection(&child, currentSubcat, categorizedBinds, conflicts)
|
||||
h.convertSection(&child, currentSubcat, categorizedBinds, conflicts, defaultKeys)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,7 +134,7 @@ func (h *HyprlandProvider) categorizeByDispatcher(dispatcher string) string {
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HyprlandProvider) convertKeybind(kb *HyprlandKeyBinding, subcategory string, conflicts map[string]*HyprlandKeyBinding) keybinds.Keybind {
|
||||
func (h *HyprlandProvider) convertKeybind(kb *HyprlandKeyBinding, subcategory string, conflicts map[string]*HyprlandKeyBinding, defaultKeys map[string]bool) keybinds.Keybind {
|
||||
keyStr := h.formatKey(kb)
|
||||
rawAction := h.formatRawAction(kb.Dispatcher, kb.Params)
|
||||
desc := kb.Comment
|
||||
@@ -143,8 +144,15 @@ func (h *HyprlandProvider) convertKeybind(kb *HyprlandKeyBinding, subcategory st
|
||||
}
|
||||
|
||||
source := "config"
|
||||
if strings.Contains(kb.Source, "dms/binds.conf") {
|
||||
if isDMSBindsUserOverridePath(kb.Source) {
|
||||
source = "dms"
|
||||
} else if isDMSBindsPrimarySourcePath(kb.Source) {
|
||||
source = "dms-default"
|
||||
}
|
||||
|
||||
hasDefault := false
|
||||
if source == "dms" && defaultKeys != nil {
|
||||
hasDefault = defaultKeys[strings.ToLower(keyStr)]
|
||||
}
|
||||
|
||||
bind := keybinds.Keybind{
|
||||
@@ -154,9 +162,10 @@ func (h *HyprlandProvider) convertKeybind(kb *HyprlandKeyBinding, subcategory st
|
||||
Subcategory: subcategory,
|
||||
Source: source,
|
||||
Flags: kb.Flags,
|
||||
HasDefault: hasDefault,
|
||||
}
|
||||
|
||||
if source == "dms" && conflicts != nil {
|
||||
if (source == "dms" || source == "dms-default") && conflicts != nil {
|
||||
normalizedKey := strings.ToLower(keyStr)
|
||||
if conflictKb, ok := conflicts[normalizedKey]; ok {
|
||||
bind.Conflict = &keybinds.Keybind{
|
||||
@@ -188,9 +197,9 @@ func (h *HyprlandProvider) formatKey(kb *HyprlandKeyBinding) string {
|
||||
func (h *HyprlandProvider) GetOverridePath() string {
|
||||
expanded, err := utils.ExpandPath(h.configPath)
|
||||
if err != nil {
|
||||
return filepath.Join(h.configPath, "dms", "binds.conf")
|
||||
return filepath.Join(h.configPath, "dms", "binds-user.lua")
|
||||
}
|
||||
return filepath.Join(expanded, "dms", "binds.conf")
|
||||
return filepath.Join(expanded, "dms", "binds-user.lua")
|
||||
}
|
||||
|
||||
func (h *HyprlandProvider) validateAction(action string) error {
|
||||
@@ -250,7 +259,16 @@ func (h *HyprlandProvider) RemoveBind(key string) error {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
normalizedKey := strings.ToLower(key)
|
||||
existingBinds[normalizedKey] = &hyprlandOverrideBind{Key: key, Unbind: true}
|
||||
return h.writeOverrideBinds(existingBinds)
|
||||
}
|
||||
|
||||
func (h *HyprlandProvider) ResetBind(key string) error {
|
||||
existingBinds, err := h.loadOverrideBinds()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
normalizedKey := strings.ToLower(key)
|
||||
delete(existingBinds, normalizedKey)
|
||||
return h.writeOverrideBinds(existingBinds)
|
||||
@@ -262,116 +280,12 @@ type hyprlandOverrideBind struct {
|
||||
Description string
|
||||
Flags string // Bind flags: l=locked, r=release, e=repeat, n=non-consuming, m=mouse, t=transparent, i=ignore-mods, s=separate, d=description, o=long-press
|
||||
Options map[string]any
|
||||
// Unbind: negative override (hl.unbind only, no rebind).
|
||||
Unbind bool
|
||||
}
|
||||
|
||||
func (h *HyprlandProvider) loadOverrideBinds() (map[string]*hyprlandOverrideBind, error) {
|
||||
overridePath := h.GetOverridePath()
|
||||
binds := make(map[string]*hyprlandOverrideBind)
|
||||
|
||||
data, err := os.ReadFile(overridePath)
|
||||
if os.IsNotExist(err) {
|
||||
return binds, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
lines := strings.Split(string(data), "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(line, "bind") {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
if len(parts) < 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Extract flags from bind type
|
||||
bindType := strings.TrimSpace(parts[0])
|
||||
flags := extractBindFlags(bindType)
|
||||
hasDescFlag := strings.Contains(flags, "d")
|
||||
|
||||
content := strings.TrimSpace(parts[1])
|
||||
commentParts := strings.SplitN(content, "#", 2)
|
||||
bindContent := strings.TrimSpace(commentParts[0])
|
||||
|
||||
var comment string
|
||||
if len(commentParts) > 1 {
|
||||
comment = strings.TrimSpace(commentParts[1])
|
||||
}
|
||||
|
||||
// For bindd, format is: mods, key, description, dispatcher, params
|
||||
var minFields, descIndex, dispatcherIndex int
|
||||
if hasDescFlag {
|
||||
minFields = 4
|
||||
descIndex = 2
|
||||
dispatcherIndex = 3
|
||||
} else {
|
||||
minFields = 3
|
||||
dispatcherIndex = 2
|
||||
}
|
||||
|
||||
fields := strings.SplitN(bindContent, ",", minFields+2)
|
||||
if len(fields) < minFields {
|
||||
continue
|
||||
}
|
||||
|
||||
mods := strings.TrimSpace(fields[0])
|
||||
keyName := strings.TrimSpace(fields[1])
|
||||
|
||||
var dispatcher, params string
|
||||
if hasDescFlag {
|
||||
if comment == "" {
|
||||
comment = strings.TrimSpace(fields[descIndex])
|
||||
}
|
||||
dispatcher = strings.TrimSpace(fields[dispatcherIndex])
|
||||
if len(fields) > dispatcherIndex+1 {
|
||||
paramParts := fields[dispatcherIndex+1:]
|
||||
params = strings.TrimSpace(strings.Join(paramParts, ","))
|
||||
}
|
||||
} else {
|
||||
dispatcher = strings.TrimSpace(fields[dispatcherIndex])
|
||||
if len(fields) > dispatcherIndex+1 {
|
||||
paramParts := fields[dispatcherIndex+1:]
|
||||
params = strings.TrimSpace(strings.Join(paramParts, ","))
|
||||
}
|
||||
}
|
||||
|
||||
keyStr := h.buildKeyString(mods, keyName)
|
||||
normalizedKey := strings.ToLower(keyStr)
|
||||
action := dispatcher
|
||||
if params != "" {
|
||||
action = dispatcher + " " + params
|
||||
}
|
||||
|
||||
binds[normalizedKey] = &hyprlandOverrideBind{
|
||||
Key: keyStr,
|
||||
Action: action,
|
||||
Description: comment,
|
||||
Flags: flags,
|
||||
}
|
||||
}
|
||||
|
||||
return binds, nil
|
||||
}
|
||||
|
||||
func (h *HyprlandProvider) buildKeyString(mods, key string) string {
|
||||
if mods == "" {
|
||||
return key
|
||||
}
|
||||
|
||||
modList := strings.FieldsFunc(mods, func(r rune) bool {
|
||||
return r == '+' || r == ' '
|
||||
})
|
||||
|
||||
parts := append(modList, key)
|
||||
return strings.Join(parts, "+")
|
||||
return readLuaOrHyprlangOverride(h.GetOverridePath())
|
||||
}
|
||||
|
||||
func (h *HyprlandProvider) getBindSortPriority(action string) int {
|
||||
@@ -420,78 +334,203 @@ func (h *HyprlandProvider) generateBindsContent(binds map[string]*hyprlandOverri
|
||||
})
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString("-- DMS user keybind overrides (edit via Control Center or dms; do not remove this header)\n\n")
|
||||
for _, bind := range bindList {
|
||||
h.writeBindLine(&sb, bind)
|
||||
writeLuaBindLine(&sb, bind)
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func (h *HyprlandProvider) writeBindLine(sb *strings.Builder, bind *hyprlandOverrideBind) {
|
||||
mods, key := h.parseKeyString(bind.Key)
|
||||
dispatcher, params := h.parseAction(bind.Action)
|
||||
|
||||
// Write bind type with flags (e.g., "bind", "binde", "bindel")
|
||||
sb.WriteString("bind")
|
||||
if bind.Flags != "" {
|
||||
sb.WriteString(bind.Flags)
|
||||
func formatLuaBindKey(internalKey string) string {
|
||||
internalKey = strings.TrimSpace(internalKey)
|
||||
parts := strings.Split(internalKey, "+")
|
||||
for i := range parts {
|
||||
parts[i] = normalizeLuaBindKeyPart(strings.TrimSpace(parts[i]))
|
||||
}
|
||||
sb.WriteString(" = ")
|
||||
sb.WriteString(mods)
|
||||
sb.WriteString(", ")
|
||||
sb.WriteString(key)
|
||||
sb.WriteString(", ")
|
||||
|
||||
// For bindd (description flag), include description before dispatcher
|
||||
if strings.Contains(bind.Flags, "d") && bind.Description != "" {
|
||||
sb.WriteString(bind.Description)
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
|
||||
sb.WriteString(dispatcher)
|
||||
|
||||
if params != "" {
|
||||
sb.WriteString(", ")
|
||||
sb.WriteString(params)
|
||||
}
|
||||
|
||||
// Only add comment if not using bindd (which has inline description)
|
||||
if bind.Description != "" && !strings.Contains(bind.Flags, "d") {
|
||||
sb.WriteString(" # ")
|
||||
sb.WriteString(bind.Description)
|
||||
}
|
||||
|
||||
sb.WriteString("\n")
|
||||
return strings.Join(parts, " + ")
|
||||
}
|
||||
|
||||
func (h *HyprlandProvider) parseKeyString(keyStr string) (mods, key string) {
|
||||
parts := strings.Split(keyStr, "+")
|
||||
switch len(parts) {
|
||||
case 0:
|
||||
return "", keyStr
|
||||
case 1:
|
||||
return "", parts[0]
|
||||
func normalizeLuaBindKeyPart(part string) string {
|
||||
switch strings.ToLower(part) {
|
||||
case "super", "mod4", "mainmod":
|
||||
return "SUPER"
|
||||
case "ctrl", "control":
|
||||
return "CTRL"
|
||||
case "shift":
|
||||
return "SHIFT"
|
||||
case "alt", "mod1":
|
||||
return "ALT"
|
||||
}
|
||||
if len(part) == 1 {
|
||||
return strings.ToUpper(part)
|
||||
}
|
||||
return part
|
||||
}
|
||||
|
||||
func luaActionStringFromHyprlangAction(action string) string {
|
||||
action = strings.TrimSpace(action)
|
||||
if strings.HasPrefix(action, "spawn ") {
|
||||
return fmt.Sprintf(`hl.dsp.exec_cmd(%s)`, strconv.Quote(strings.TrimSpace(strings.TrimPrefix(action, "spawn "))))
|
||||
}
|
||||
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 strings.Join(parts[:len(parts)-1], " "), parts[len(parts)-1]
|
||||
return fmt.Sprintf(`hl.dsp.exec_cmd(%s)`, strconv.Quote("hyprctl dispatch "+action))
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HyprlandProvider) parseAction(action string) (dispatcher, params string) {
|
||||
parts := strings.SplitN(action, " ", 2)
|
||||
switch len(parts) {
|
||||
case 0:
|
||||
return action, ""
|
||||
case 1:
|
||||
dispatcher = parts[0]
|
||||
default:
|
||||
dispatcher = parts[0]
|
||||
params = parts[1]
|
||||
func luaExprToInternalAction(expr string) string {
|
||||
d, p := luaExprToDispatcherParams(expr)
|
||||
if d == "exec" && p != "" && !strings.HasPrefix(p, "hyprctl dispatch lua:") {
|
||||
return "exec " + p
|
||||
}
|
||||
|
||||
// Convert internal spawn format to Hyprland's exec
|
||||
if dispatcher == "spawn" {
|
||||
dispatcher = "exec"
|
||||
if p != "" {
|
||||
return d + " " + p
|
||||
}
|
||||
|
||||
return dispatcher, params
|
||||
return d
|
||||
}
|
||||
|
||||
func luaBindOptions(bind *hyprlandOverrideBind) []string {
|
||||
var opts []string
|
||||
if strings.Contains(bind.Flags, "l") {
|
||||
opts = append(opts, "locked = true")
|
||||
}
|
||||
if strings.Contains(bind.Flags, "e") {
|
||||
opts = append(opts, "repeating = true")
|
||||
}
|
||||
if bind.Description != "" && strings.Contains(bind.Flags, "d") {
|
||||
opts = append(opts, fmt.Sprintf("description = %s", strconv.Quote(bind.Description)))
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
||||
func writeLuaBindLine(sb *strings.Builder, bind *hyprlandOverrideBind) {
|
||||
key := formatLuaBindKey(bind.Key)
|
||||
if bind.Unbind {
|
||||
fmt.Fprintf(sb, `hl.unbind("%s")`, key)
|
||||
sb.WriteByte('\n')
|
||||
return
|
||||
}
|
||||
expr := luaActionStringFromHyprlangAction(bind.Action)
|
||||
opts := luaBindOptions(bind)
|
||||
fmt.Fprintf(sb, `hl.unbind("%s")`, key)
|
||||
sb.WriteByte('\n')
|
||||
if len(opts) > 0 {
|
||||
fmt.Fprintf(sb, `hl.bind("%s", %s, { %s })`, key, expr, strings.Join(opts, ", "))
|
||||
} else {
|
||||
if bind.Description != "" {
|
||||
fmt.Fprintf(sb, `hl.bind("%s", %s) -- %s`, key, expr, bind.Description)
|
||||
} else {
|
||||
fmt.Fprintf(sb, `hl.bind("%s", %s)`, key, expr)
|
||||
}
|
||||
}
|
||||
sb.WriteByte('\n')
|
||||
}
|
||||
|
||||
func parseLuaBindOverrideLine(line string) (*hyprlandOverrideBind, bool) {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || strings.HasPrefix(line, "--") {
|
||||
return nil, false
|
||||
}
|
||||
kbc, actionExpr, optSuffix, ok := parseLuaBindInvocation(line)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
internalKey := luaKeyComboToInternalKey(kbc)
|
||||
|
||||
action := luaExprToInternalAction(actionExpr)
|
||||
flags := luaBindOptFlags(optSuffix)
|
||||
description := luaBindOptDescription(optSuffix)
|
||||
return &hyprlandOverrideBind{
|
||||
Key: internalKey,
|
||||
Action: action,
|
||||
Description: description,
|
||||
Flags: flags,
|
||||
}, true
|
||||
}
|
||||
|
||||
func parseLuaUnbindLine(line string) (string, bool) {
|
||||
line = strings.TrimSpace(line)
|
||||
if !strings.HasPrefix(line, "hl.unbind") {
|
||||
return "", false
|
||||
}
|
||||
rest := strings.TrimSpace(line[len("hl.unbind"):])
|
||||
if !strings.HasPrefix(rest, "(") {
|
||||
return "", false
|
||||
}
|
||||
rest = rest[1:]
|
||||
combo, _, ok := parseLuaStringLiteral(rest, 0)
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
return luaKeyComboToInternalKey(combo), true
|
||||
}
|
||||
|
||||
func luaKeyComboToInternalKey(combo string) string {
|
||||
parts := strings.Fields(strings.ReplaceAll(strings.ReplaceAll(combo, "+", " "), " ", " "))
|
||||
return strings.Join(parts, "+")
|
||||
}
|
||||
|
||||
func readLuaOrHyprlangOverride(path string) (map[string]*hyprlandOverrideBind, error) {
|
||||
binds := make(map[string]*hyprlandOverrideBind)
|
||||
data, err := os.ReadFile(path)
|
||||
if os.IsNotExist(err) {
|
||||
return binds, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lines := strings.Split(string(data), "\n")
|
||||
parser := NewHyprlandParser("")
|
||||
pendingUnbinds := make(map[string]string)
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || strings.HasPrefix(line, "--") {
|
||||
continue
|
||||
}
|
||||
if key, ok := parseLuaUnbindLine(line); ok {
|
||||
pendingUnbinds[strings.ToLower(key)] = key
|
||||
continue
|
||||
}
|
||||
if kb, ok := parseLuaBindOverrideLine(line); ok {
|
||||
normalizedKey := strings.ToLower(kb.Key)
|
||||
binds[normalizedKey] = kb
|
||||
delete(pendingUnbinds, normalizedKey)
|
||||
continue
|
||||
}
|
||||
if !strings.HasPrefix(line, "bind") {
|
||||
continue
|
||||
}
|
||||
kb := parser.parseBindLine(line)
|
||||
if kb == nil {
|
||||
continue
|
||||
}
|
||||
keyStr := parser.formatBindKey(kb)
|
||||
action := kb.Dispatcher
|
||||
if kb.Params != "" {
|
||||
action = kb.Dispatcher + " " + kb.Params
|
||||
}
|
||||
flags := kb.Flags
|
||||
normalizedKey := strings.ToLower(keyStr)
|
||||
binds[normalizedKey] = &hyprlandOverrideBind{
|
||||
Key: keyStr,
|
||||
Action: action,
|
||||
Description: kb.Comment,
|
||||
Flags: flags,
|
||||
}
|
||||
delete(pendingUnbinds, normalizedKey)
|
||||
}
|
||||
for normKey, origKey := range pendingUnbinds {
|
||||
binds[normKey] = &hyprlandOverrideBind{Key: origKey, Unbind: true}
|
||||
}
|
||||
return binds, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user