diff options
| author | crecher <crecher@crecher.net> | 2026-10-26 01:24:42 +0000 |
|---|---|---|
| committer | crecher <crecher@crecher.net> | 2026-10-26 01:24:42 +0000 |
| commit | e3ffc7f8d80914eee07a1feaed3869e445d1dbfe (patch) | |
| tree | 709cff7fecd95f6044c3acfdb1680972fbdde695 /main.go | |
| parent | ec6258504f7348a2f2364037bdd9142f7defcc5e (diff) | |
| download | wm-e3ffc7f8d80914eee07a1feaed3869e445d1dbfe.tar.gz wm-e3ffc7f8d80914eee07a1feaed3869e445d1dbfe.zip | |
Signed-off-by: crecher <crecher@crecher.net>
Diffstat (limited to 'main.go')
| -rw-r--r-- | main.go | 262 |
1 files changed, 246 insertions, 16 deletions
@@ -29,6 +29,12 @@ type Key struct { Fn func(*xgb.Conn) } +type MouseBind struct { + Mod uint16 + Button byte + Resize bool +} + type Client struct { Win xproto.Window X int16 @@ -117,6 +123,14 @@ func handleEvent(conn *xgb.Conn, ev xgb.Event) { switch e := ev.(type) { case xproto.KeyPressEvent: handleKeyPress(conn, e) + case xproto.ConfigureRequestEvent: + handleConfigureRequest(conn, e) + case xproto.ButtonPressEvent: + handleButtonPress(conn, e) + case xproto.MotionNotifyEvent: + handleMotion(conn, e) + case xproto.ButtonReleaseEvent: + drag.c = nil case xproto.MapRequestEvent: manageWindow(conn, e.Window) case xproto.DestroyNotifyEvent: @@ -156,6 +170,7 @@ func setup(conn *xgb.Conn) { } grabKeys(conn) + grabButtons(conn) if barHeight != 0 { createBar(conn) drawBar(conn) @@ -308,6 +323,97 @@ func grabKeys(conn *xgb.Conn) { } } +var drag struct { + c *Client + resize bool + px, py int16 + x, y int16 + width, height uint16 +} + +const minFloatSize = 50 + +func grabButtons(conn *xgb.Conn) { + root := xproto.Setup(conn).DefaultScreen(conn).Root + + for _, b := range mouseBinds { + for _, m := range []uint16{ + 0, + xproto.ModMaskLock, + xproto.ModMask2, + xproto.ModMaskLock | xproto.ModMask2, + } { + xproto.GrabButton(conn, + false, + root, + xproto.EventMaskButtonPress| + xproto.EventMaskButtonRelease| + xproto.EventMaskButtonMotion, + xproto.GrabModeAsync, + xproto.GrabModeAsync, + xproto.WindowNone, + xproto.CursorNone, + b.Button, + b.Mod|m, + ) + } + } +} + +func handleButtonPress(conn *xgb.Conn, e xproto.ButtonPressEvent) { + ignoredMods := uint16(xproto.ModMaskLock) | uint16(xproto.ModMask2) + state := e.State &^ ignoredMods + var bind *MouseBind + for i := range mouseBinds { + if mouseBinds[i].Button == byte(e.Detail) && mouseBinds[i].Mod == state { + bind = &mouseBinds[i] + break + } + } + if bind == nil { + return + } + c, ok := clients[e.Child] + if !ok || !c.Floating { + return + } + + focused = c.Win + setInputFocus(conn, focused) + drawBar(conn) + xproto.ConfigureWindow(conn, c.Win, xproto.ConfigWindowStackMode, []uint32{xproto.StackModeAbove}) + + drag.c = c + drag.resize = bind.Resize + drag.px, drag.py = e.RootX, e.RootY + drag.x, drag.y = c.X, c.Y + drag.width, drag.height = c.Width, c.Height +} + +func handleMotion(conn *xgb.Conn, e xproto.MotionNotifyEvent) { + c := drag.c + if c == nil || clients[c.Win] != c { + return + } + dx := int(e.RootX) - int(drag.px) + dy := int(e.RootY) - int(drag.py) + + var mask uint16 + var vals []uint32 + if drag.resize { + c.Width = uint16(max(int(drag.width)+dx, minFloatSize)) + c.Height = uint16(max(int(drag.height)+dy, minFloatSize)) + mask = xproto.ConfigWindowWidth | xproto.ConfigWindowHeight + vals = []uint32{uint32(c.Width), uint32(c.Height)} + } else { + c.X = int16(int(drag.x) + dx) + c.Y = int16(int(drag.y) + dy) + mask = xproto.ConfigWindowX | xproto.ConfigWindowY + vals = []uint32{uint32(int32(c.X)), uint32(int32(c.Y))} + } + xproto.ConfigureWindow(conn, c.Win, mask, vals) +} + func keysymToKeycode(conn *xgb.Conn, sym uint32) xproto.Keycode { setup := xproto.Setup(conn) min := setup.MinKeycode @@ -447,12 +553,7 @@ func moveToTag(conn *xgb.Conn, tags BitMask) { } func zoom(conn *xgb.Conn) { - var tiled []*Client - for _, c := range ActiveClients { - if !c.Floating { - tiled = append(tiled, c) - } - } + tiled := tiledClients() c, ok := clients[focused] if !ok || c.Floating || len(tiled) < 2 { return @@ -488,6 +589,69 @@ func switchTags(conn *xgb.Conn, tags BitMask) { drawBar(conn) } +func tiledClients() []*Client { + var t []*Client + for _, c := range ActiveClients { + if !c.Floating { + t = append(t, c) + } + } + return t +} + +func toggleFloating(conn *xgb.Conn) { + c, ok := clients[focused] + if !ok { + return + } + c.Floating = !c.Floating + if c.Floating { + if g, err := xproto.GetGeometry(conn, xproto.Drawable(c.Win)).Reply(); err == nil { + c.X, c.Y, c.Width, c.Height = g.X, g.Y, g.Width, g.Height + } + } + updateWindows(conn) +} + +func handleConfigureRequest(conn *xgb.Conn, e xproto.ConfigureRequestEvent) { + if c, ok := clients[e.Window]; ok { + if c.Floating { + if e.ValueMask&xproto.ConfigWindowX != 0 { + c.X = e.X + } + if e.ValueMask&xproto.ConfigWindowY != 0 { + c.Y = e.Y + } + if e.ValueMask&xproto.ConfigWindowWidth != 0 { + c.Width = e.Width + } + if e.ValueMask&xproto.ConfigWindowHeight != 0 { + c.Height = e.Height + } + updateWindows(conn) + } + return + } + + mask := e.ValueMask &^ (xproto.ConfigWindowSibling | xproto.ConfigWindowStackMode) + var vals []uint32 + for _, f := range []struct { + bit uint16 + val uint32 + }{ + {xproto.ConfigWindowX, uint32(int32(e.X))}, + {xproto.ConfigWindowY, uint32(int32(e.Y))}, + {xproto.ConfigWindowWidth, uint32(e.Width)}, + {xproto.ConfigWindowHeight, uint32(e.Height)}, + {xproto.ConfigWindowBorderWidth, uint32(e.BorderWidth)}, + } { + if mask&f.bit != 0 { + vals = append(vals, f.val) + } + } + xproto.ConfigureWindow(conn, e.Window, mask, vals) +} + func dim(v int) uint32 { v -= borderWidth * 2 if v < 1 { @@ -502,7 +666,8 @@ func tileVertical(conn *xgb.Conn) { usableHeight := int(screen.HeightInPixels) - int(barHeight) - n := len(ActiveClients) + tiled := tiledClients() + n := len(tiled) if n == 0 { return } @@ -510,10 +675,10 @@ func tileVertical(conn *xgb.Conn) { heightPerWin := usableHeight / n i := 0 - for _, c := range ActiveClients { + for _, c := range tiled { yOffset := i * heightPerWin height := heightPerWin - gapSize - if i == len(ActiveClients)-1 { + if i == n-1 { height = heightPerWin - gapSize*2 } xproto.ConfigureWindow(conn, c.Win, @@ -537,17 +702,18 @@ func tileHorizontal(conn *xgb.Conn) { screenWidth := int(screen.WidthInPixels) screenHeight := int(screen.HeightInPixels) - n := len(ActiveClients) + tiled := tiledClients() + n := len(tiled) if n == 0 { return } widthPerWin := screenWidth / n i := 0 - for _, c := range ActiveClients { + for _, c := range tiled { xOffset := int(i * widthPerWin) width := widthPerWin - gapSize // log.Println(i, len(ActiveClients)) - if i == len(ActiveClients)-1 { + if i == n-1 { width = widthPerWin - gapSize*2 } xproto.ConfigureWindow(conn, c.Win, @@ -571,11 +737,12 @@ func tileWithMaster(conn *xgb.Conn) { screenHeight := int(screen.HeightInPixels) usableHeight := int(screen.HeightInPixels) - int(barHeight) - n := len(ActiveClients) + tiled := tiledClients() + n := len(tiled) if n == 0 { return } else if n == 1 { - xproto.ConfigureWindow(conn, ActiveClients[0].Win, + xproto.ConfigureWindow(conn, tiled[0].Win, xproto.ConfigWindowX| xproto.ConfigWindowY| xproto.ConfigWindowWidth| @@ -599,7 +766,7 @@ func tileWithMaster(conn *xgb.Conn) { masterWidth = screenWidth } - for i, c := range ActiveClients { + for i, c := range tiled { col, count := i, masters x, width := gapSize, masterWidth-gapSize if stack == 0 { @@ -895,6 +1062,24 @@ func updateWindows(conn *xgb.Conn) { log.Fatal("Unknown layout ", layout) } + for _, c := range ActiveClients { + if c.Floating { + xproto.ConfigureWindow(conn, c.Win, + xproto.ConfigWindowX| + xproto.ConfigWindowY| + xproto.ConfigWindowWidth| + xproto.ConfigWindowHeight| + xproto.ConfigWindowStackMode, + []uint32{ + uint32(int32(c.X)), + uint32(int32(c.Y)), + uint32(c.Width), + uint32(c.Height), + xproto.StackModeAbove, + }) + } + } + refocus(conn) updateBorders(conn) } @@ -919,6 +1104,7 @@ var ( mod uint16 startup []string keys []Key + mouseBinds []MouseBind ) const shift = uint16(xproto.ModMaskShift) @@ -973,7 +1159,8 @@ type fileConfig struct { Border string `toml:"border"` BorderFocus string `toml:"border_focus"` } `toml:"colors"` - Binds map[string]string `toml:"binds"` + Binds map[string]string `toml:"binds"` + MouseBinds map[string]string `toml:"mousebinds"` } // userConfigPath returns $XDG_CONFIG_HOME/wm/config.toml (default ~/.config/wm/config.toml). @@ -1059,6 +1246,29 @@ func loadConfig() { } keys = append(keys, Key{Mod: m, Sym: sym, Fn: fn}) } + + combos = combos[:0] + for combo := range cfg.MouseBinds { + combos = append(combos, combo) + } + sort.Strings(combos) + mouseBinds = nil + for _, combo := range combos { + action := strings.TrimSpace(cfg.MouseBinds[combo]) + if action == "" { + continue + } + m, button, err := parseButtonCombo(combo) + if err != nil { + log.Printf("config: mousebind %q: %v", combo, err) + continue + } + if action != "move" && action != "resize" { + log.Printf("config: mousebind %q: unknown action %q", combo, action) + continue + } + mouseBinds = append(mouseBinds, MouseBind{Mod: m, Button: button, Resize: action == "resize"}) + } } func writeDefaultConfig(path string) error { @@ -1121,6 +1331,24 @@ func parseCombo(s string) (uint16, uint32, error) { return 0, 0, fmt.Errorf("unknown key %q", name) } +// parseButtonCombo parses "mod+button1" into modifiers and a button number. +func parseButtonCombo(s string) (uint16, byte, error) { + parts := strings.Split(s, "+") + name := strings.ToLower(strings.TrimSpace(parts[len(parts)-1])) + var m uint16 + if len(parts) > 1 { + var err error + if m, err = parseMods(strings.Join(parts[:len(parts)-1], "+")); err != nil { + return 0, 0, err + } + } + n, err := strconv.Atoi(strings.TrimPrefix(name, "button")) + if !strings.HasPrefix(name, "button") || err != nil || n < 1 || n > 5 { + return 0, 0, fmt.Errorf("unknown button %q, want button1-button5", name) + } + return m, byte(n), nil +} + // parseAction turns "spawn st", "view 2", "layout tileVertical", ... into a handler. func parseAction(s string) (func(*xgb.Conn), error) { name, arg, _ := strings.Cut(s, " ") @@ -1137,6 +1365,8 @@ func parseAction(s string) (func(*xgb.Conn), error) { return func(*xgb.Conn) { restart() }, nil case "zoom": return zoom, nil + case "float": + return toggleFloating, nil case "kill": return func(conn *xgb.Conn) { killActiveWindow(conn, focused) }, nil case "layout": |