mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-06-07 19:59:14 -04:00
1192 lines
35 KiB
Go
1192 lines
35 KiB
Go
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 isKnownHyprlandDispatcher(dispatcher string) bool {
|
|
switch dispatcher {
|
|
case "exec", "execr", "spawn",
|
|
"killactive", "forcekillactive", "closewindow", "killwindow",
|
|
"signal", "signalwindow", "togglefloating", "setfloating", "settiled",
|
|
"workspace", "renameworkspace", "fullscreen", "fullscreenstate", "fakefullscreen",
|
|
"movetoworkspace", "movetoworkspacesilent", "pseudo", "movefocus",
|
|
"movewindow", "swapwindow", "centerwindow", "togglegroup", "changegroupactive",
|
|
"movegroupwindow", "focusmonitor", "movecursortocorner", "movecursor",
|
|
"workspaceopt", "exit", "movecurrentworkspacetomonitor", "focusworkspaceoncurrentmonitor",
|
|
"moveworkspacetomonitor", "togglespecialworkspace", "forcerendererreload",
|
|
"resizeactive", "moveactive", "cyclenext", "focuswindowbyclass", "focuswindow",
|
|
"tagwindow", "toggleswallow", "submap", "pass", "sendshortcut", "sendkeystate",
|
|
"layoutmsg", "splitratio", "dpms", "movewindowpixel", "resizewindowpixel",
|
|
"swapnext", "swapactiveworkspaces", "pin", "mouse", "bringactivetotop",
|
|
"alterzorder", "focusurgentorlast", "focuscurrentorlast", "lockgroups",
|
|
"lockactivegroup", "moveintogroup", "moveoutofgroup", "movewindoworgroup",
|
|
"moveintoorcreategroup", "setignoregrouplock", "denywindowfromgroup", "event",
|
|
"global", "setprop", "forceidle":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
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 dispatcherWindowMoveResize(funcName, params string) string {
|
|
geometry, window := splitCommaParams(params)
|
|
x, y, relative, ok := xyParams(geometry)
|
|
if !ok {
|
|
return ""
|
|
}
|
|
if !isBareLuaNumber(x) || !isBareLuaNumber(y) {
|
|
return ""
|
|
}
|
|
fields := []luaField{
|
|
luaNumberOrStringField("x", x),
|
|
luaNumberOrStringField("y", y),
|
|
luaBoolField("relative", relative),
|
|
}
|
|
if window != "" {
|
|
fields = append(fields, luaStringField("window", window))
|
|
}
|
|
return luaDispatcherTableCall(funcName, fields...)
|
|
}
|
|
|
|
func splitCommaParams(params string) (left, right string) {
|
|
left = strings.TrimSpace(params)
|
|
if idx := strings.Index(left, ","); idx >= 0 {
|
|
right = strings.TrimSpace(left[idx+1:])
|
|
left = strings.TrimSpace(left[:idx])
|
|
}
|
|
return left, right
|
|
}
|
|
|
|
func luaHyprctlDispatchFunction(action string) string {
|
|
return fmt.Sprintf(`function() hl.exec_cmd(%s) end`, strconv.Quote("hyprctl dispatch "+strings.TrimSpace(action)))
|
|
}
|
|
|
|
func luaToggleActionValue(params string) string {
|
|
switch strings.ToLower(strings.TrimSpace(params)) {
|
|
case "on", "enable", "enabled", "set", "lock":
|
|
return "on"
|
|
case "off", "disable", "disabled", "unset", "unlock":
|
|
return "off"
|
|
default:
|
|
return "toggle"
|
|
}
|
|
}
|
|
|
|
func dispatcherToggleTableCall(funcName, params string) string {
|
|
return luaDispatcherTableCall(funcName, luaStringField("action", luaToggleActionValue(params)))
|
|
}
|
|
|
|
func dispatcherCycleNext(params string) string {
|
|
params = strings.TrimSpace(strings.ToLower(params))
|
|
if params == "" {
|
|
return `hl.dsp.window.cycle_next()`
|
|
}
|
|
fields := []luaField{}
|
|
for _, field := range strings.Fields(params) {
|
|
switch field {
|
|
case "prev", "previous", "b":
|
|
fields = append(fields, luaBoolField("next", false))
|
|
case "next", "f":
|
|
fields = append(fields, luaBoolField("next", true))
|
|
case "tiled":
|
|
fields = append(fields, luaBoolField("tiled", true))
|
|
case "floating":
|
|
fields = append(fields, luaBoolField("floating", true))
|
|
}
|
|
}
|
|
if len(fields) == 0 {
|
|
return ""
|
|
}
|
|
return luaDispatcherTableCall("hl.dsp.window.cycle_next", fields...)
|
|
}
|
|
|
|
func dispatcherSwapNext(params string) string {
|
|
switch strings.ToLower(strings.TrimSpace(params)) {
|
|
case "prev", "previous", "b":
|
|
return `hl.dsp.window.swap({ prev = true })`
|
|
default:
|
|
return `hl.dsp.window.swap({ next = true })`
|
|
}
|
|
}
|
|
|
|
func dispatcherGroupActive(params string) string {
|
|
switch strings.ToLower(strings.TrimSpace(params)) {
|
|
case "f", "next", "forward":
|
|
return `hl.dsp.group.next()`
|
|
case "b", "prev", "previous", "backward":
|
|
return `hl.dsp.group.prev()`
|
|
}
|
|
if isBareLuaNumber(params) {
|
|
return luaDispatcherTableCall("hl.dsp.group.active", luaNumberOrStringField("index", params))
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func dispatcherMoveGroupWindow(params string) string {
|
|
switch strings.ToLower(strings.TrimSpace(params)) {
|
|
case "b", "prev", "previous", "backward":
|
|
return `hl.dsp.group.move_window({ forward = false })`
|
|
default:
|
|
return `hl.dsp.group.move_window({ forward = true })`
|
|
}
|
|
}
|
|
|
|
func dispatcherCursorMove(params string) string {
|
|
x, y, _, ok := xyParams(params)
|
|
if !ok || !isBareLuaNumber(x) || !isBareLuaNumber(y) {
|
|
return ""
|
|
}
|
|
return luaDispatcherTableCall("hl.dsp.cursor.move", luaNumberOrStringField("x", x), luaNumberOrStringField("y", y))
|
|
}
|
|
|
|
func dispatcherSignal(params string) string {
|
|
signal, window := firstParam(params)
|
|
if signal == "" || !isBareLuaNumber(signal) {
|
|
return ""
|
|
}
|
|
fields := []luaField{luaNumberOrStringField("signal", signal)}
|
|
if window != "" {
|
|
fields = append(fields, luaStringField("window", window))
|
|
}
|
|
return luaDispatcherTableCall("hl.dsp.window.signal", fields...)
|
|
}
|
|
|
|
func dispatcherSignalWindow(params string) string {
|
|
window, rest := firstParam(params)
|
|
signal, _ := firstParam(rest)
|
|
if signal == "" || !isBareLuaNumber(signal) {
|
|
return ""
|
|
}
|
|
fields := []luaField{luaNumberOrStringField("signal", signal)}
|
|
if window != "" {
|
|
fields = append(fields, luaStringField("window", window))
|
|
}
|
|
return luaDispatcherTableCall("hl.dsp.window.signal", fields...)
|
|
}
|
|
|
|
func dispatcherTagWindow(params string) string {
|
|
tag, window := firstParam(params)
|
|
if tag == "" {
|
|
return ""
|
|
}
|
|
fields := []luaField{luaStringField("tag", tag)}
|
|
if window != "" {
|
|
fields = append(fields, luaStringField("window", window))
|
|
}
|
|
return luaDispatcherTableCall("hl.dsp.window.tag", fields...)
|
|
}
|
|
|
|
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 "execr":
|
|
return fmt.Sprintf(`hl.dsp.exec_raw(%s)`, strconv.Quote(params)), true
|
|
case "killactive":
|
|
return `hl.dsp.window.kill()`, true
|
|
case "forcekillactive":
|
|
return `hl.dsp.window.kill()`, true
|
|
case "closewindow":
|
|
if params == "" {
|
|
return `hl.dsp.window.close()`, true
|
|
}
|
|
return luaDispatcherTableCall("hl.dsp.window.close", luaStringField("window", params)), true
|
|
case "killwindow":
|
|
if params == "" {
|
|
return `hl.dsp.window.kill()`, true
|
|
}
|
|
return luaDispatcherTableCall("hl.dsp.window.kill", luaStringField("window", params)), true
|
|
case "togglefloating":
|
|
return dispatcherToggleTableCall("hl.dsp.window.float", "toggle"), true
|
|
case "setfloating":
|
|
return dispatcherToggleTableCall("hl.dsp.window.float", "on"), true
|
|
case "settiled":
|
|
return dispatcherToggleTableCall("hl.dsp.window.float", "off"), 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
|
|
}
|
|
return luaHyprctlDispatchFunction(action), 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 "fakefullscreen":
|
|
return luaHyprctlDispatchFunction(action), true
|
|
case "pin":
|
|
if params == "" {
|
|
return `hl.dsp.window.pin()`, true
|
|
}
|
|
return dispatcherToggleTableCall("hl.dsp.window.pin", params), true
|
|
case "pseudo":
|
|
return dispatcherToggleTableCall("hl.dsp.window.pseudo", params), 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 "swapnext":
|
|
return dispatcherSwapNext(params), true
|
|
case "resizeactive":
|
|
if expr := dispatcherActiveMoveResize("hl.dsp.window.resize", params); expr != "" {
|
|
return expr, true
|
|
}
|
|
return luaHyprctlDispatchFunction(action), true
|
|
case "moveactive":
|
|
if expr := dispatcherActiveMoveResize("hl.dsp.window.move", params); expr != "" {
|
|
return expr, true
|
|
}
|
|
return luaHyprctlDispatchFunction(action), true
|
|
case "resizewindowpixel":
|
|
if expr := dispatcherWindowMoveResize("hl.dsp.window.resize", params); expr != "" {
|
|
return expr, true
|
|
}
|
|
return luaHyprctlDispatchFunction(action), true
|
|
case "movewindowpixel":
|
|
if expr := dispatcherWindowMoveResize("hl.dsp.window.move", params); expr != "" {
|
|
return expr, true
|
|
}
|
|
return luaHyprctlDispatchFunction(action), 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 "workspaceopt":
|
|
return luaHyprctlDispatchFunction(action), 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 "focuswindowbyclass":
|
|
if params != "" {
|
|
return luaDispatcherTableCall("hl.dsp.focus", luaStringField("window", "class:"+params)), true
|
|
}
|
|
case "focuscurrentorlast":
|
|
return `hl.dsp.focus({ last = true })`, true
|
|
case "focusurgentorlast":
|
|
return `hl.dsp.focus({ urgent_or_last = true })`, true
|
|
case "cyclenext":
|
|
if expr := dispatcherCycleNext(params); expr != "" {
|
|
return expr, true
|
|
}
|
|
return luaHyprctlDispatchFunction(action), true
|
|
case "layoutmsg":
|
|
if params != "" {
|
|
return fmt.Sprintf(`hl.dsp.layout(%s)`, strconv.Quote(params)), true
|
|
}
|
|
case "splitratio":
|
|
return luaHyprctlDispatchFunction(action), 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 "bringactivetotop":
|
|
return `hl.dsp.window.bring_to_top()`, true
|
|
case "toggleswallow":
|
|
return `hl.dsp.window.toggle_swallow()`, true
|
|
case "signal":
|
|
if expr := dispatcherSignal(params); expr != "" {
|
|
return expr, true
|
|
}
|
|
case "signalwindow":
|
|
if expr := dispatcherSignalWindow(params); expr != "" {
|
|
return expr, true
|
|
}
|
|
case "tagwindow":
|
|
if expr := dispatcherTagWindow(params); expr != "" {
|
|
return expr, 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 "movecursortocorner":
|
|
if params != "" && isBareLuaNumber(params) {
|
|
return luaDispatcherTableCall("hl.dsp.cursor.move_to_corner", luaNumberOrStringField("corner", params)), true
|
|
}
|
|
case "movecursor":
|
|
if expr := dispatcherCursorMove(params); expr != "" {
|
|
return expr, true
|
|
}
|
|
case "togglegroup":
|
|
return `hl.dsp.group.toggle()`, true
|
|
case "changegroupactive":
|
|
if expr := dispatcherGroupActive(params); expr != "" {
|
|
return expr, true
|
|
}
|
|
return luaHyprctlDispatchFunction(action), true
|
|
case "movegroupwindow":
|
|
return dispatcherMoveGroupWindow(params), true
|
|
case "moveintogroup":
|
|
if params != "" {
|
|
return luaDispatcherTableCall("hl.dsp.window.move", luaStringField("into_group", params)), true
|
|
}
|
|
case "moveintoorcreategroup":
|
|
if params != "" {
|
|
return luaDispatcherTableCall("hl.dsp.window.move", luaStringField("into_or_create_group", params)), true
|
|
}
|
|
case "moveoutofgroup":
|
|
if params != "" {
|
|
return luaDispatcherTableCall("hl.dsp.window.move", luaStringField("out_of_group", params)), true
|
|
}
|
|
return luaDispatcherTableCall("hl.dsp.window.move", luaBoolField("out_of_group", true)), true
|
|
case "movewindoworgroup":
|
|
if params != "" {
|
|
return luaDispatcherTableCall("hl.dsp.window.move", luaStringField("direction", params), luaBoolField("group_aware", true)), true
|
|
}
|
|
case "lockgroups":
|
|
return dispatcherToggleTableCall("hl.dsp.group.lock", params), true
|
|
case "lockactivegroup":
|
|
return dispatcherToggleTableCall("hl.dsp.group.lock_active", params), true
|
|
case "denywindowfromgroup":
|
|
return dispatcherToggleTableCall("hl.dsp.window.deny_from_group", params), true
|
|
case "setignoregrouplock":
|
|
return luaHyprctlDispatchFunction(action), true
|
|
case "forcerendererreload":
|
|
return `hl.dsp.force_renderer_reload()`, true
|
|
case "forceidle":
|
|
if params != "" && isBareLuaNumber(params) {
|
|
return fmt.Sprintf(`hl.dsp.force_idle(%s)`, params), true
|
|
}
|
|
}
|
|
if isKnownHyprlandDispatcher(dispatcher) {
|
|
return luaHyprctlDispatchFunction(action), true
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
func luaActionStringFromHyprlangAction(action string) string {
|
|
action = strings.TrimSpace(action)
|
|
if expr, ok := luaActionStringFromKnownHyprlandAction(action); ok {
|
|
return expr
|
|
}
|
|
return 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
|
|
}
|