package providers import ( "fmt" "os" "path/filepath" "sort" "strconv" "strings" "github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds" "github.com/AvengeMedia/DankMaterialShell/core/internal/utils" ) type HyprlandProvider struct { configPath string dmsBindsIncluded bool parsed bool } func NewHyprlandProvider(configPath string) *HyprlandProvider { if configPath == "" { configPath = defaultHyprlandConfigDir() } return &HyprlandProvider{ configPath: configPath, } } func defaultHyprlandConfigDir() string { configDir, err := os.UserConfigDir() if err != nil { return "" } return filepath.Join(configDir, "hypr") } func (h *HyprlandProvider) Name() string { return "hyprland" } func (h *HyprlandProvider) GetCheatSheet() (*keybinds.CheatSheet, error) { result, err := ParseHyprlandKeysWithDMS(h.configPath) if err != nil { return nil, fmt.Errorf("failed to parse hyprland config: %w", err) } h.dmsBindsIncluded = result.DMSBindsIncluded h.parsed = true categorizedBinds := make(map[string][]keybinds.Keybind) h.convertSection(result.Section, "", categorizedBinds, result.ConflictingConfigs, result.DefaultDMSKeys) sheet := &keybinds.CheatSheet{ Title: "Hyprland Keybinds", Provider: h.Name(), Binds: categorizedBinds, DMSBindsIncluded: result.DMSBindsIncluded, } if result.DMSStatus != nil { sheet.DMSStatus = &keybinds.DMSBindsStatus{ Exists: result.DMSStatus.Exists, Included: result.DMSStatus.Included, IncludePosition: result.DMSStatus.IncludePosition, TotalIncludes: result.DMSStatus.TotalIncludes, BindsAfterDMS: result.DMSStatus.BindsAfterDMS, Effective: result.DMSStatus.Effective, OverriddenBy: result.DMSStatus.OverriddenBy, StatusMessage: result.DMSStatus.StatusMessage, ConfigFormat: result.DMSStatus.ConfigFormat, ReadOnly: result.DMSStatus.ReadOnly, } } return sheet, nil } func (h *HyprlandProvider) HasDMSBindsIncluded() bool { if h.parsed { return h.dmsBindsIncluded } result, err := ParseHyprlandKeysWithDMS(h.configPath) if err != nil { return false } h.dmsBindsIncluded = result.DMSBindsIncluded h.parsed = true return h.dmsBindsIncluded } 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 } for _, kb := range section.Keybinds { category := h.categorizeByDispatcher(kb.Dispatcher) bind := h.convertKeybind(&kb, currentSubcat, conflicts, defaultKeys) categorizedBinds[category] = append(categorizedBinds[category], bind) } for _, child := range section.Children { h.convertSection(&child, currentSubcat, categorizedBinds, conflicts, defaultKeys) } } func (h *HyprlandProvider) categorizeByDispatcher(dispatcher string) string { switch { case strings.Contains(dispatcher, "workspace"): return "Workspace" case strings.Contains(dispatcher, "monitor"): return "Monitor" case strings.Contains(dispatcher, "window") || strings.Contains(dispatcher, "focus") || strings.Contains(dispatcher, "move") || strings.Contains(dispatcher, "swap") || strings.Contains(dispatcher, "resize") || dispatcher == "killactive" || dispatcher == "fullscreen" || dispatcher == "togglefloating" || dispatcher == "pin" || dispatcher == "fakefullscreen" || dispatcher == "splitratio" || dispatcher == "resizeactive": return "Window" case dispatcher == "exec": return "Execute" case dispatcher == "exit" || strings.Contains(dispatcher, "dpms"): return "System" default: return "Other" } } 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 if desc == "" { desc = rawAction } source := "config" 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{ Key: keyStr, Description: desc, Action: rawAction, Subcategory: subcategory, Source: source, Flags: kb.Flags, HasDefault: hasDefault, } if (source == "dms" || source == "dms-default") && conflicts != nil { normalizedKey := strings.ToLower(keyStr) if conflictKb, ok := conflicts[normalizedKey]; ok { bind.Conflict = &keybinds.Keybind{ Key: keyStr, Description: conflictKb.Comment, Action: h.formatRawAction(conflictKb.Dispatcher, conflictKb.Params), Source: "config", } } } return bind } func (h *HyprlandProvider) formatRawAction(dispatcher, params string) string { if params != "" { return dispatcher + " " + params } return dispatcher } func (h *HyprlandProvider) formatKey(kb *HyprlandKeyBinding) string { parts := make([]string, 0, len(kb.Mods)+1) parts = append(parts, kb.Mods...) parts = append(parts, kb.Key) return strings.Join(parts, "+") } func (h *HyprlandProvider) GetOverridePath() string { expanded, err := utils.ExpandPath(h.configPath) if err != nil { return filepath.Join(h.configPath, "dms", "binds-user.lua") } return filepath.Join(expanded, "dms", "binds-user.lua") } func (h *HyprlandProvider) validateAction(action string) error { action = strings.TrimSpace(action) switch { case action == "": return fmt.Errorf("action cannot be empty") case action == "exec" || action == "exec ": return fmt.Errorf("exec dispatcher requires arguments") case strings.HasPrefix(action, "exec "): rest := strings.TrimSpace(strings.TrimPrefix(action, "exec ")) if rest == "" { return fmt.Errorf("exec dispatcher requires arguments") } } return nil } 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 { return err } overridePath := h.GetOverridePath() if err := os.MkdirAll(filepath.Dir(overridePath), 0o755); err != nil { return fmt.Errorf("failed to create dms directory: %w", err) } existingBinds, err := h.loadOverrideBinds() if err != nil { existingBinds = make(map[string]*hyprlandOverrideBind) } // Extract flags from options var flags string if options != nil { if f, ok := options["flags"].(string); ok { flags = f } } canonicalKey := canonicalHyprlandOverrideKey(key) normalizedKey := hyprlandOverrideMapKey(canonicalKey) existingBinds[normalizedKey] = &hyprlandOverrideBind{ Key: canonicalKey, Action: action, Description: description, Flags: flags, Options: options, } return h.writeOverrideBinds(existingBinds) } func (h *HyprlandProvider) RemoveBind(key string) error { if err := h.ensureWritableConfig(); err != nil { return err } existingBinds, err := h.loadOverrideBinds() if err != nil { return nil } canonicalKey := canonicalHyprlandOverrideKey(key) normalizedKey := hyprlandOverrideMapKey(canonicalKey) existingBinds[normalizedKey] = &hyprlandOverrideBind{Key: canonicalKey, Unbind: true} return h.writeOverrideBinds(existingBinds) } func (h *HyprlandProvider) ResetBind(key string) error { if err := h.ensureWritableConfig(); err != nil { return err } existingBinds, err := h.loadOverrideBinds() if err != nil { return nil } normalizedKey := hyprlandOverrideMapKey(key) delete(existingBinds, normalizedKey) return h.writeOverrideBinds(existingBinds) } type hyprlandOverrideBind struct { Key string Action string 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) 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) { 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 { switch { case strings.HasPrefix(action, "exec") && strings.Contains(action, "dms"): return 0 case strings.Contains(action, "workspace"): return 1 case strings.Contains(action, "window") || strings.Contains(action, "focus") || strings.Contains(action, "move") || strings.Contains(action, "swap") || strings.Contains(action, "resize"): return 2 case strings.Contains(action, "monitor"): return 3 case strings.HasPrefix(action, "exec"): return 4 case action == "exit" || strings.Contains(action, "dpms"): return 5 default: return 6 } } func (h *HyprlandProvider) writeOverrideBinds(binds map[string]*hyprlandOverrideBind) error { overridePath := h.GetOverridePath() content := h.generateBindsContent(binds) return os.WriteFile(overridePath, []byte(content), 0o644) } func (h *HyprlandProvider) generateBindsContent(binds map[string]*hyprlandOverrideBind) string { if len(binds) == 0 { return "" } bindList := make([]*hyprlandOverrideBind, 0, len(binds)) for _, bind := range binds { bindList = append(bindList, bind) } sort.Slice(bindList, func(i, j int) bool { pi, pj := h.getBindSortPriority(bindList[i].Action), h.getBindSortPriority(bindList[j].Action) if pi != pj { return pi < pj } return bindList[i].Key < bindList[j].Key }) 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 { writeLuaBindLine(&sb, bind) } return sb.String() } func formatLuaBindKey(internalKey string) string { internalKey = strings.TrimSpace(internalKey) parts := strings.Split(internalKey, "+") for i := range parts { parts[i] = normalizeLuaBindKeyPart(strings.TrimSpace(parts[i])) } return strings.Join(parts, " + ") } 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 } 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 { action = strings.TrimSpace(action) if expr, ok := luaActionStringFromKnownHyprlandAction(action); ok { return expr } return fmt.Sprintf(`hl.dispatch(%s)`, strconv.Quote(action)) } func luaExprToInternalAction(expr string) string { d, p := luaExprToDispatcherParams(expr) if d == "exec" && p != "" && !strings.HasPrefix(p, "hyprctl dispatch lua:") { return "exec " + p } if p != "" { return d + " " + p } 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 != "" { 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 { 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) if description == "" { description = luaLineTrailingComment(line) } 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[hyprlandOverrideMapKey(key)] = canonicalHyprlandOverrideKey(key) continue } if kb, ok := parseLuaBindOverrideLine(line); ok { kb.Key = canonicalHyprlandOverrideKey(kb.Key) normalizedKey := hyprlandOverrideMapKey(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 keyStr = canonicalHyprlandOverrideKey(keyStr) normalizedKey := hyprlandOverrideMapKey(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 }