aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--default_config.toml5
-rw-r--r--main.go262
2 files changed, 251 insertions, 16 deletions
diff --git a/default_config.toml b/default_config.toml
index 98edfba..d5e8d8a 100644
--- a/default_config.toml
+++ b/default_config.toml
@@ -26,6 +26,7 @@ border_focus = "#285577"
"mod+shift+r" = "restart"
"mod+q" = "kill"
"mod+shift+Return" = "zoom"
+"mod+shift+space" = "float"
"mod+shift+h" = "layout tileHorizontal"
"mod+shift+v" = "layout tileVertical"
"mod+shift+m" = "layout tileWithMaster"
@@ -62,3 +63,7 @@ border_focus = "#285577"
"mod+9" = "view 9"
"mod+shift+9" = "tag 9"
"alt+9" = "toggle 9"
+
+[mousebinds]
+"mod+button1" = "move"
+"mod+button3" = "resize"
diff --git a/main.go b/main.go
index 74caab4..e781316 100644
--- a/main.go
+++ b/main.go
@@ -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":