mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-06-23 19:45:21 -04:00
@@ -190,9 +190,13 @@ func (h *HyprlandProvider) formatRawAction(dispatcher, params string) string {
|
||||
}
|
||||
|
||||
func (h *HyprlandProvider) formatKey(kb *HyprlandKeyBinding) string {
|
||||
key := kb.Key
|
||||
if canonical, ok := hyprlandScrollToCanonical(key); ok {
|
||||
key = canonical
|
||||
}
|
||||
parts := make([]string, 0, len(kb.Mods)+1)
|
||||
parts = append(parts, kb.Mods...)
|
||||
parts = append(parts, kb.Key)
|
||||
parts = append(parts, key)
|
||||
return strings.Join(parts, "+")
|
||||
}
|
||||
|
||||
@@ -411,6 +415,9 @@ func normalizeLuaBindKeyPart(part string) string {
|
||||
case "alt", "mod1":
|
||||
return "ALT"
|
||||
}
|
||||
if native, ok := hyprlandScrollToNative(part); ok {
|
||||
return native
|
||||
}
|
||||
if len(part) == 1 {
|
||||
return strings.ToUpper(part)
|
||||
}
|
||||
@@ -1130,6 +1137,11 @@ func parseLuaUnbindLine(line string) (string, bool) {
|
||||
|
||||
func luaKeyComboToInternalKey(combo string) string {
|
||||
parts := strings.Fields(strings.ReplaceAll(strings.ReplaceAll(combo, "+", " "), " ", " "))
|
||||
for i, part := range parts {
|
||||
if canonical, ok := hyprlandScrollToCanonical(part); ok {
|
||||
parts[i] = canonical
|
||||
}
|
||||
}
|
||||
return strings.Join(parts, "+")
|
||||
}
|
||||
|
||||
|
||||
@@ -347,9 +347,13 @@ func (p *HyprlandParser) buildDMSStatus() *HyprlandDMSStatus {
|
||||
}
|
||||
|
||||
func (p *HyprlandParser) formatBindKey(kb *HyprlandKeyBinding) string {
|
||||
key := kb.Key
|
||||
if canonical, ok := hyprlandScrollToCanonical(key); ok {
|
||||
key = canonical
|
||||
}
|
||||
parts := make([]string, 0, len(kb.Mods)+1)
|
||||
parts = append(parts, kb.Mods...)
|
||||
parts = append(parts, kb.Key)
|
||||
parts = append(parts, key)
|
||||
return strings.Join(parts, "+")
|
||||
}
|
||||
|
||||
|
||||
@@ -486,6 +486,61 @@ hl.bind("SUPER + 1", hl.dsp.exec_cmd("hyprctl dispatch workspace 1"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestHyprlandSetBindTranslatesScrollWheelToMouse(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
dmsDir := filepath.Join(tmpDir, "dms")
|
||||
if err := os.MkdirAll(dmsDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bindsUser := filepath.Join(dmsDir, "binds-user.lua")
|
||||
if err := os.WriteFile(bindsUser, []byte("-- DMS user keybind overrides\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
provider := NewHyprlandProvider(tmpDir)
|
||||
if err := provider.SetBind("SUPER + WheelScrollDown", "workspace 1", "", nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
got := readFile(t, bindsUser)
|
||||
if !strings.Contains(got, `hl.bind("SUPER + mouse_down"`) {
|
||||
t.Fatalf("expected scroll key translated to mouse_down, got:\n%s", got)
|
||||
}
|
||||
if strings.Contains(got, "WheelScroll") {
|
||||
t.Fatalf("expected no raw niri scroll keysym in hyprland output, got:\n%s", got)
|
||||
}
|
||||
|
||||
if err := provider.SetBind("SUPER + WheelScrollDown", "workspace 2", "", nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got = readFile(t, bindsUser)
|
||||
if strings.Count(got, `hl.bind("SUPER + mouse_down"`) != 1 {
|
||||
t.Fatalf("expected exactly one mouse_down bind after re-save, got:\n%s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHyprlandScrollWheelRoundTrips(t *testing.T) {
|
||||
for native, canonical := range map[string]string{
|
||||
"mouse_up": "WheelScrollUp",
|
||||
"mouse_down": "WheelScrollDown",
|
||||
"mouse_left": "WheelScrollLeft",
|
||||
"mouse_right": "WheelScrollRight",
|
||||
} {
|
||||
if got := luaKeyComboToInternalKey("SUPER + " + native); got != "SUPER+"+canonical {
|
||||
t.Errorf("luaKeyComboToInternalKey(%q) = %q, want SUPER+%s", native, got, canonical)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func readFile(t *testing.T, path string) string {
|
||||
t.Helper()
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func TestHyprlandRemoveBindReplacesExistingOverrideWithNegativeOverride(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
dmsDir := filepath.Join(tmpDir, "dms")
|
||||
|
||||
@@ -236,6 +236,9 @@ func (m *MangoWCProvider) SetBind(key, action, description string, options map[s
|
||||
if optionPrefix := m.bindPrefixFromOptions(options); optionPrefix != "" {
|
||||
prefix = optionPrefix
|
||||
}
|
||||
if _, leaf := m.parseKeyString(key); isScrollKey(leaf) {
|
||||
prefix = mangowcAxisBindPrefix
|
||||
}
|
||||
|
||||
existingBinds[normalizedKey] = &mangowcOverrideBind{
|
||||
Key: key,
|
||||
@@ -346,6 +349,12 @@ func (m *MangoWCProvider) parseOverrideBindLine(line, precedingComment string) (
|
||||
keyName := strings.TrimSpace(fields[1])
|
||||
command := strings.TrimSpace(fields[2])
|
||||
|
||||
if prefix == mangowcAxisBindPrefix {
|
||||
if canonical, ok := mangowcDirectionToScroll(keyName); ok {
|
||||
keyName = canonical
|
||||
}
|
||||
}
|
||||
|
||||
var params string
|
||||
if len(fields) > 3 {
|
||||
params = strings.TrimSpace(fields[3])
|
||||
@@ -365,6 +374,9 @@ func (m *MangoWCProvider) parseOverrideBindLine(line, precedingComment string) (
|
||||
}
|
||||
|
||||
func (m *MangoWCProvider) isBindPrefix(prefix string) bool {
|
||||
if prefix == mangowcAxisBindPrefix {
|
||||
return true
|
||||
}
|
||||
if !strings.HasPrefix(prefix, "bind") {
|
||||
return false
|
||||
}
|
||||
@@ -591,6 +603,11 @@ func (m *MangoWCProvider) writeBindLine(sb *strings.Builder, bind *mangowcOverri
|
||||
if prefix == "" {
|
||||
prefix = "bind"
|
||||
}
|
||||
if prefix == mangowcAxisBindPrefix {
|
||||
if direction, ok := mangowcScrollToDirection(key); ok {
|
||||
key = direction
|
||||
}
|
||||
}
|
||||
sb.WriteString(prefix)
|
||||
sb.WriteString("=")
|
||||
if mods == "" {
|
||||
|
||||
@@ -244,7 +244,7 @@ func (p *MangoWCParser) ParseKeys() []MangoWCKeyBinding {
|
||||
}
|
||||
continue
|
||||
}
|
||||
if !strings.HasPrefix(trimmed, "bind") {
|
||||
if !strings.HasPrefix(trimmed, "bind") && !strings.HasPrefix(trimmed, mangowcAxisBindPrefix) {
|
||||
pendingComment = ""
|
||||
continue
|
||||
}
|
||||
@@ -427,7 +427,7 @@ func (p *MangoWCParser) parseFileWithSource(filePath string) ([]MangoWCKeyBindin
|
||||
continue
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(trimmed, "bind") {
|
||||
if !strings.HasPrefix(trimmed, "bind") && !strings.HasPrefix(trimmed, mangowcAxisBindPrefix) {
|
||||
pendingComment = ""
|
||||
continue
|
||||
}
|
||||
@@ -493,7 +493,7 @@ func (p *MangoWCParser) parseDMSBindsDirectly(dmsBindsPath string) []MangoWCKeyB
|
||||
// line directly above) is the description: mango feeds inline comments to spawn
|
||||
// as argv, so DMS keeps descriptions on the line above; inline `#` is a fallback.
|
||||
func (p *MangoWCParser) getKeybindAtLineContent(line string, precedingComment string) *MangoWCKeyBinding {
|
||||
bindMatch := regexp.MustCompile(`^(bind[lsrp]*)\s*=\s*(.+)$`)
|
||||
bindMatch := regexp.MustCompile(`^(bind[lsrp]*|axisbind)\s*=\s*(.+)$`)
|
||||
matches := bindMatch.FindStringSubmatch(line)
|
||||
if len(matches) < 3 {
|
||||
return nil
|
||||
@@ -527,6 +527,12 @@ func (p *MangoWCParser) getKeybindAtLineContent(line string, precedingComment st
|
||||
key := strings.TrimSpace(keyFields[1])
|
||||
command := strings.TrimSpace(keyFields[2])
|
||||
|
||||
if matches[1] == mangowcAxisBindPrefix {
|
||||
if canonical, ok := mangowcDirectionToScroll(key); ok {
|
||||
key = canonical
|
||||
}
|
||||
}
|
||||
|
||||
var params string
|
||||
if len(keyFields) > 3 {
|
||||
params = strings.TrimSpace(keyFields[3])
|
||||
|
||||
@@ -6,6 +6,29 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMangoWCParseAxisBindToScrollKey(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
cfg := filepath.Join(tmpDir, "config.conf")
|
||||
content := "axisbind=SUPER,UP,spawn,dms ipc call test\n"
|
||||
if err := os.WriteFile(cfg, []byte(content), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
binds, err := ParseMangoWCKeys(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseMangoWCKeys failed: %v", err)
|
||||
}
|
||||
if len(binds) != 1 {
|
||||
t.Fatalf("expected 1 bind, got %d", len(binds))
|
||||
}
|
||||
if binds[0].Key != "WheelScrollUp" {
|
||||
t.Fatalf("expected axis direction parsed as WheelScrollUp, got %q", binds[0].Key)
|
||||
}
|
||||
if len(binds[0].Mods) != 1 || binds[0].Mods[0] != "SUPER" {
|
||||
t.Fatalf("expected SUPER mod, got %v", binds[0].Mods)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMangoWCAutogenerateComment(t *testing.T) {
|
||||
tests := []struct {
|
||||
command string
|
||||
|
||||
@@ -417,6 +417,40 @@ bind=SUPER,3,view,3
|
||||
}
|
||||
}
|
||||
|
||||
func TestMangoWCSetBindTranslatesScrollWheelToAxisBind(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
dmsDir := filepath.Join(tmpDir, "dms")
|
||||
if err := os.MkdirAll(dmsDir, 0o755); err != nil {
|
||||
t.Fatalf("failed to create dms dir: %v", err)
|
||||
}
|
||||
bindsPath := filepath.Join(dmsDir, "binds.conf")
|
||||
seed := "# === Custom Keybinds ===\nbind=SUPER,t,spawn,ghostty\ngesturebind=none,left,3,focusdir,left\n"
|
||||
if err := os.WriteFile(bindsPath, []byte(seed), 0o644); err != nil {
|
||||
t.Fatalf("failed to write seed binds: %v", err)
|
||||
}
|
||||
|
||||
provider := NewMangoWCProvider(tmpDir)
|
||||
if err := provider.SetBind("SUPER+WheelScrollDown", "spawn dms ipc call test", "Scroll down", nil); err != nil {
|
||||
t.Fatalf("SetBind failed: %v", err)
|
||||
}
|
||||
|
||||
content := readFile(t, bindsPath)
|
||||
if !strings.Contains(content, "axisbind=SUPER,DOWN,spawn,dms ipc call test") {
|
||||
t.Fatalf("expected scroll bind written as axisbind direction, got:\n%s", content)
|
||||
}
|
||||
if strings.Contains(content, "WheelScroll") {
|
||||
t.Fatalf("expected no raw niri scroll keysym in mango output, got:\n%s", content)
|
||||
}
|
||||
|
||||
if err := provider.SetBind("SUPER+WheelScrollDown", "spawn dms ipc call test2", "Scroll down", nil); err != nil {
|
||||
t.Fatalf("SetBind failed: %v", err)
|
||||
}
|
||||
content = readFile(t, bindsPath)
|
||||
if strings.Count(content, "axisbind=SUPER,DOWN,") != 1 {
|
||||
t.Fatalf("expected exactly one axisbind after re-save, got:\n%s", content)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMangoWCRemoveBindPreservesNonBindLines(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
dmsDir := filepath.Join(tmpDir, "dms")
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package providers
|
||||
|
||||
import "strings"
|
||||
|
||||
// Scroll-wheel binds are captured by the shell as niri's keysym names
|
||||
// (WheelScrollUp/Down/Left/Right) regardless of the active compositor. Niri
|
||||
// consumes them natively; every other provider speaks a different dialect, so the
|
||||
// raw niri token must be translated on write and back again on read. Without this
|
||||
// the token is emitted verbatim and the compositor rejects the bind (issue #2683).
|
||||
|
||||
var canonicalScrollKeys = map[string]string{
|
||||
"wheelscrollup": "WheelScrollUp",
|
||||
"wheelscrolldown": "WheelScrollDown",
|
||||
"wheelscrollleft": "WheelScrollLeft",
|
||||
"wheelscrollright": "WheelScrollRight",
|
||||
}
|
||||
|
||||
func isScrollKey(token string) bool {
|
||||
_, ok := canonicalScrollKeys[strings.ToLower(token)]
|
||||
return ok
|
||||
}
|
||||
|
||||
// Hyprland binds the wheel inside a regular bind using mouse_up/down/left/right.
|
||||
var hyprlandScrollNative = map[string]string{
|
||||
"wheelscrollup": "mouse_up",
|
||||
"wheelscrolldown": "mouse_down",
|
||||
"wheelscrollleft": "mouse_left",
|
||||
"wheelscrollright": "mouse_right",
|
||||
}
|
||||
|
||||
var hyprlandScrollCanonical = map[string]string{
|
||||
"mouse_up": "WheelScrollUp",
|
||||
"mouse_down": "WheelScrollDown",
|
||||
"mouse_left": "WheelScrollLeft",
|
||||
"mouse_right": "WheelScrollRight",
|
||||
}
|
||||
|
||||
func hyprlandScrollToNative(token string) (string, bool) {
|
||||
v, ok := hyprlandScrollNative[strings.ToLower(token)]
|
||||
return v, ok
|
||||
}
|
||||
|
||||
func hyprlandScrollToCanonical(token string) (string, bool) {
|
||||
v, ok := hyprlandScrollCanonical[strings.ToLower(token)]
|
||||
return v, ok
|
||||
}
|
||||
|
||||
// MangoWC binds the wheel through a dedicated axisbind directive whose key field
|
||||
// is a direction (UP/DOWN/LEFT/RIGHT) rather than a keysym.
|
||||
const mangowcAxisBindPrefix = "axisbind"
|
||||
|
||||
var mangowcScrollDirection = map[string]string{
|
||||
"wheelscrollup": "UP",
|
||||
"wheelscrolldown": "DOWN",
|
||||
"wheelscrollleft": "LEFT",
|
||||
"wheelscrollright": "RIGHT",
|
||||
}
|
||||
|
||||
var mangowcScrollCanonical = map[string]string{
|
||||
"up": "WheelScrollUp",
|
||||
"down": "WheelScrollDown",
|
||||
"left": "WheelScrollLeft",
|
||||
"right": "WheelScrollRight",
|
||||
}
|
||||
|
||||
func mangowcScrollToDirection(token string) (string, bool) {
|
||||
v, ok := mangowcScrollDirection[strings.ToLower(token)]
|
||||
return v, ok
|
||||
}
|
||||
|
||||
func mangowcDirectionToScroll(direction string) (string, bool) {
|
||||
v, ok := mangowcScrollCanonical[strings.ToLower(direction)]
|
||||
return v, ok
|
||||
}
|
||||
Reference in New Issue
Block a user