aboutsummaryrefslogtreecommitdiff
path: root/main.go
diff options
context:
space:
mode:
authorcrecher <crecher@crecher.net>2026-10-26 00:19:42 +0000
committercrecher <crecher@crecher.net>2026-10-26 00:19:42 +0000
commit1db9b13964082042a9578d372723777273c50daa (patch)
tree7c60b907298b4d2c70f5e0fedecd608db03aaee8 /main.go
parent4e257e15b4ddc25ff222613491f7e16354d2aeb3 (diff)
downloadwm-1db9b13964082042a9578d372723777273c50daa.tar.gz
wm-1db9b13964082042a9578d372723777273c50daa.zip
added configuring via .config file!!
Signed-off-by: crecher <crecher@crecher.net>
Diffstat (limited to 'main.go')
-rw-r--r--main.go418
1 files changed, 356 insertions, 62 deletions
diff --git a/main.go b/main.go
index cb16435..9d6af05 100644
--- a/main.go
+++ b/main.go
@@ -2,13 +2,21 @@ package main
import (
"bufio"
+ _ "embed"
+ "fmt"
"log"
"os"
"os/exec"
+ "path/filepath"
+ "sort"
"strconv"
+ "strings"
+ "github.com/BurntSushi/toml"
"github.com/BurntSushi/xgb"
"github.com/BurntSushi/xgb/xproto"
+
+ "github.com/crecheer/wm/keysym"
)
// types
@@ -44,6 +52,7 @@ var (
var (
barWindow xproto.Window
barGC xproto.Gcontext
+ barPixmap xproto.Pixmap // offscreen buffer, copied to barWindow to avoid flicker
fontID xproto.Font
fontOK bool
fontAscent int
@@ -53,7 +62,11 @@ var (
)
// layout
-var layout string = "tileWithMaster"
+var (
+ layout string = "tileWithMaster"
+ masterCount int = 1
+ masterRatio float64 = 0.5
+)
// tags
var (
@@ -66,6 +79,7 @@ func main() {
if err != nil {
log.Fatal(err)
}
+ loadConfig()
setup(conn)
events := make(chan xgb.Event)
@@ -535,71 +549,61 @@ func tileWithMaster(conn *xgb.Conn) {
screenHeight := int(screen.HeightInPixels)
usableHeight := int(screen.HeightInPixels) - int(barHeight)
- var fullscreenMaster int = 0
-
n := len(ActiveClients)
if n == 0 {
return
} else if n == 1 {
- fullscreenMaster = 1
+ xproto.ConfigureWindow(conn, ActiveClients[0].Win,
+ xproto.ConfigWindowX|
+ xproto.ConfigWindowY|
+ xproto.ConfigWindowWidth|
+ xproto.ConfigWindowHeight,
+ []uint32{
+ uint32(0), // x
+ uint32(0), // y
+ dim(screenWidth), // width
+ dim(screenHeight - int(barHeight)),
+ })
+ return
}
- widthPerWin := screenWidth / 2
- masterClient := ActiveClients[0]
- i := 0
- for _, c := range ActiveClients {
- if c == masterClient {
- if fullscreenMaster == 1 {
- xproto.ConfigureWindow(conn, c.Win,
- xproto.ConfigWindowX|
- xproto.ConfigWindowY|
- xproto.ConfigWindowWidth|
- xproto.ConfigWindowHeight,
- []uint32{
- uint32(0), // x
- uint32(0), // y
- dim(screenWidth), // width
- dim(screenHeight - int(barHeight)),
- })
- } else {
- xproto.ConfigureWindow(conn, c.Win,
- xproto.ConfigWindowX|
- xproto.ConfigWindowY|
- xproto.ConfigWindowWidth|
- xproto.ConfigWindowHeight,
- []uint32{
- uint32(0 + gapSize), // x
- uint32(gapSize), // y
- dim(screenWidth/2 - gapSize), // width
- dim(screenHeight - int(barHeight) - gapSize*2),
- })
- }
- } else {
- heightPerWin := usableHeight / (n - 1)
- yOffset := (i * heightPerWin) + gapSize
- xOffset := uint32(widthPerWin + gapSize)
- height := heightPerWin - gapSize*2
- // log.Println(len(ActiveClients), i)
- if n > 2 {
- height = heightPerWin - gapSize
- }
- if i >= len(ActiveClients)-2 {
- height = heightPerWin - gapSize*2
- }
- xproto.ConfigureWindow(conn, c.Win,
- xproto.ConfigWindowX|
- xproto.ConfigWindowY|
- xproto.ConfigWindowWidth|
- xproto.ConfigWindowHeight,
- []uint32{
- xOffset, // x
- uint32(yOffset), // y
- dim(widthPerWin - gapSize*2),
- dim(height),
- })
- i++
+ masters := min(masterCount, n)
+ stack := n - masters
+
+ masterWidth := int(float64(screenWidth) * masterRatio)
+ xOffset := masterWidth + gapSize
+ stackWidth := screenWidth - masterWidth - gapSize*2
+ if stack == 0 {
+ masterWidth = screenWidth
+ }
+
+ for i, c := range ActiveClients {
+ col, count := i, masters
+ x, width := gapSize, masterWidth-gapSize
+ if stack == 0 {
+ width = masterWidth - gapSize*2
+ }
+ if i >= masters {
+ col, count = i-masters, stack
+ x, width = xOffset, stackWidth
+ }
+ heightPerWin := usableHeight / count
+ height := heightPerWin - gapSize
+ if col == count-1 {
+ height = heightPerWin - gapSize*2
}
+ xproto.ConfigureWindow(conn, c.Win,
+ xproto.ConfigWindowX|
+ xproto.ConfigWindowY|
+ xproto.ConfigWindowWidth|
+ xproto.ConfigWindowHeight,
+ []uint32{
+ uint32(x), // x
+ uint32(col*heightPerWin + gapSize), // y
+ dim(width),
+ dim(height),
+ })
}
}
@@ -712,7 +716,7 @@ func drawBarText(conn *xgb.Conn, x, top, height int, text string, fg, bg uint32)
baseline := top + (height+fontAscent-fontDescent)/2
if xftOK {
- xftDrawText(barWindow, x, baseline, text, fg)
+ xftDrawText(xproto.Window(barPixmap), x, baseline, text, fg)
return
}
@@ -731,7 +735,7 @@ func drawBarText(conn *xgb.Conn, x, top, height int, text string, fg, bg uint32)
copy(textItem[2:], ascii)
xproto.ChangeGC(conn, barGC, xproto.GcForeground|xproto.GcBackground, []uint32{fg, bg})
- xproto.PolyText8(conn, xproto.Drawable(barWindow), barGC, int16(x), int16(baseline), textItem)
+ xproto.PolyText8(conn, xproto.Drawable(barPixmap), barGC, int16(x), int16(baseline), textItem)
}
func drawBar(conn *xgb.Conn) {
@@ -741,7 +745,19 @@ func drawBar(conn *xgb.Conn) {
screen := xproto.Setup(conn).DefaultScreen(conn)
barWidth := int(screen.WidthInPixels)
- xproto.ClearArea(conn, false, barWindow, 0, 0, screen.WidthInPixels, barHeight)
+ if barPixmap == 0 {
+ barPixmap, _ = xproto.NewPixmapId(conn)
+ xproto.CreatePixmap(conn, screen.RootDepth, barPixmap, xproto.Drawable(barWindow), screen.WidthInPixels, barHeight)
+ }
+ xproto.ChangeGC(conn, barGC, xproto.GcForeground, []uint32{barColor})
+ xproto.PolyFillRectangle(conn, xproto.Drawable(barPixmap), barGC,
+ []xproto.Rectangle{{X: 0, Y: 0, Width: screen.WidthInPixels, Height: barHeight}})
+ // xft draws over its own connection; make sure the fill has landed first
+ xproto.GetInputFocus(conn).Reply()
+ defer func() {
+ xproto.CopyArea(conn, xproto.Drawable(barPixmap), xproto.Drawable(barWindow), barGC,
+ 0, 0, 0, 0, screen.WidthInPixels, barHeight)
+ }()
stw := 0
if statusText != "" {
@@ -771,7 +787,7 @@ func drawBar(conn *xgb.Conn) {
x += textWidth(conn, label) + int(barHeight)*2/3
}
- if windowTitle != 0 {
+ if windowTitle {
mw := barWidth - stw - x
if mw > int(barHeight) {
title := getWindowTitle(conn, focused)
@@ -860,3 +876,281 @@ func updateWindows(conn *xgb.Conn) {
refocus(conn)
updateBorders(conn)
}
+
+//go:embed default_config.toml
+var defaultConfig []byte
+
+// appearance and behaviour, filled in by loadConfig
+var (
+ barHeight uint16
+ gapSize int
+ barColor uint32
+ barFg uint32
+ selBg uint32
+ selFg uint32
+ fontName string
+ tagCount int
+ borderWidth int
+ borderColor uint32
+ borderFocus uint32
+ windowTitle bool
+ mod uint16
+ startup []string
+ keys []Key
+)
+
+const shift = uint16(xproto.ModMaskShift)
+
+const (
+ minRatio = 0.1
+ maxRatio = 0.9
+)
+
+func run(cmd string, args ...string) func(*xgb.Conn) {
+ return func(*xgb.Conn) { spawn(cmd, args...) }
+}
+
+func setLayout(name string) func(*xgb.Conn) {
+ return func(conn *xgb.Conn) {
+ layout = name
+ updateWindows(conn)
+ }
+}
+
+func incMasters(delta int) func(*xgb.Conn) {
+ return func(conn *xgb.Conn) {
+ masterCount = max(masterCount+delta, 1)
+ updateWindows(conn)
+ }
+}
+
+func incRatio(delta float64) func(*xgb.Conn) {
+ return func(conn *xgb.Conn) {
+ masterRatio = min(max(masterRatio+delta, minRatio), maxRatio)
+ updateWindows(conn)
+ }
+}
+
+type fileConfig struct {
+ Gap int `toml:"gap"`
+ BorderWidth int `toml:"border_width"`
+ BarHeight uint16 `toml:"bar_height"`
+ Font string `toml:"font"`
+ Tags int `toml:"tags"`
+ Layout string `toml:"layout"`
+ MasterCount int `toml:"master_count"`
+ MasterRatio float64 `toml:"master_ratio"`
+ Mod string `toml:"mod"`
+ WindowTitle bool `toml:"window_title"`
+ Startup []string `toml:"startup"`
+ Colors struct {
+ BarBg string `toml:"bar_bg"`
+ BarFg string `toml:"bar_fg"`
+ SelBg string `toml:"sel_bg"`
+ SelFg string `toml:"sel_fg"`
+ Border string `toml:"border"`
+ BorderFocus string `toml:"border_focus"`
+ } `toml:"colors"`
+ Binds map[string]string `toml:"binds"`
+}
+
+// userConfigPath returns $XDG_CONFIG_HOME/wm/config.toml (default ~/.config/wm/config.toml).
+func userConfigPath() string {
+ dir := os.Getenv("XDG_CONFIG_HOME")
+ if dir == "" {
+ home, err := os.UserHomeDir()
+ if err != nil {
+ return ""
+ }
+ dir = filepath.Join(home, ".config")
+ }
+ return filepath.Join(dir, "wm", "config.toml")
+}
+
+// loadConfig applies the embedded defaults, then the user's config on top. If
+// the user has no config yet, the default is copied into place first.
+func loadConfig() {
+ var cfg fileConfig
+ if _, err := toml.Decode(string(defaultConfig), &cfg); err != nil {
+ log.Fatal("bad built-in config: ", err)
+ }
+
+ if path := userConfigPath(); path != "" {
+ if _, err := os.Stat(path); os.IsNotExist(err) {
+ if err := writeDefaultConfig(path); err != nil {
+ log.Println("config: could not write default config:", err)
+ } else {
+ log.Println("config: wrote default config to", path)
+ }
+ } else if md, err := toml.DecodeFile(path, &cfg); err != nil {
+ log.Printf("config %s: %v (using defaults)", path, err)
+ } else {
+ for _, k := range md.Undecoded() {
+ log.Printf("config %s: unknown option %q", path, k.String())
+ }
+ }
+ }
+
+ gapSize, borderWidth, barHeight = cfg.Gap, cfg.BorderWidth, cfg.BarHeight
+ fontName, layout, windowTitle, startup = cfg.Font, cfg.Layout, cfg.WindowTitle, cfg.Startup
+ tagCount = min(max(cfg.Tags, 1), 31)
+ masterCount = max(cfg.MasterCount, 1)
+ masterRatio = min(max(cfg.MasterRatio, minRatio), maxRatio)
+
+ mod = 0
+ var err error
+ if mod, err = parseMods(cfg.Mod); err != nil || mod == 0 {
+ log.Printf("config: bad mod %q, using super", cfg.Mod)
+ mod = uint16(xproto.ModMask4)
+ }
+
+ for dst, val := range map[*uint32]string{
+ &barColor: cfg.Colors.BarBg, &barFg: cfg.Colors.BarFg,
+ &selBg: cfg.Colors.SelBg, &selFg: cfg.Colors.SelFg,
+ &borderColor: cfg.Colors.Border, &borderFocus: cfg.Colors.BorderFocus,
+ } {
+ if err := setColor(dst, val); err != nil {
+ log.Println("config:", err)
+ }
+ }
+
+ combos := make([]string, 0, len(cfg.Binds))
+ for combo := range cfg.Binds {
+ combos = append(combos, combo)
+ }
+ sort.Strings(combos)
+ keys = nil
+ for _, combo := range combos {
+ action := strings.TrimSpace(cfg.Binds[combo])
+ if action == "" {
+ continue
+ }
+ m, sym, err := parseCombo(combo)
+ if err != nil {
+ log.Printf("config: bind %q: %v", combo, err)
+ continue
+ }
+ fn, err := parseAction(action)
+ if err != nil {
+ log.Printf("config: bind %q: %v", combo, err)
+ continue
+ }
+ keys = append(keys, Key{Mod: m, Sym: sym, Fn: fn})
+ }
+}
+
+func writeDefaultConfig(path string) error {
+ if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
+ return err
+ }
+ return os.WriteFile(path, defaultConfig, 0o644)
+}
+
+func setColor(dst *uint32, val string) error {
+ hex := strings.TrimPrefix(val, "#")
+ n, err := strconv.ParseUint(hex, 16, 32)
+ if err != nil || len(hex) != 6 {
+ return fmt.Errorf("bad color %q, want #RRGGBB", val)
+ }
+ *dst = uint32(n)
+ return nil
+}
+
+// parseMods parses "super+shift"; "mod" stands for the configured mod key.
+func parseMods(s string) (uint16, error) {
+ var m uint16
+ for _, p := range strings.Split(s, "+") {
+ switch strings.ToLower(strings.TrimSpace(p)) {
+ case "mod":
+ m |= mod
+ case "super", "win", "mod4":
+ m |= uint16(xproto.ModMask4)
+ case "alt", "mod1":
+ m |= uint16(xproto.ModMask1)
+ case "shift":
+ m |= shift
+ case "ctrl", "control":
+ m |= uint16(xproto.ModMaskControl)
+ default:
+ return 0, fmt.Errorf("unknown modifier %q", p)
+ }
+ }
+ return m, nil
+}
+
+// parseCombo parses "mod+shift+Return" into modifiers and a keysym.
+func parseCombo(s string) (uint16, uint32, error) {
+ parts := strings.Split(s, "+")
+ name := strings.TrimSpace(parts[len(parts)-1])
+ m, err := parseMods(strings.Join(parts[:len(parts)-1], "+"))
+ if len(parts) == 1 {
+ m, err = 0, nil
+ }
+ if err != nil {
+ return 0, 0, err
+ }
+ name = strings.TrimPrefix(name, "XK_")
+ if sym, ok := keysym.Names[name]; ok {
+ return m, sym, nil
+ }
+ if len(name) == 1 {
+ return m, uint32(name[0]), nil
+ }
+ return 0, 0, fmt.Errorf("unknown key %q", name)
+}
+
+// parseAction turns "spawn st", "view 2", "layout tileVertical", ... into a handler.
+func parseAction(s string) (func(*xgb.Conn), error) {
+ name, arg, _ := strings.Cut(s, " ")
+ arg = strings.TrimSpace(arg)
+ switch name {
+ case "spawn":
+ if arg == "" {
+ return nil, fmt.Errorf("spawn needs a command")
+ }
+ return run("sh", "-c", arg), nil
+ case "quit":
+ return func(*xgb.Conn) { quit() }, nil
+ case "restart":
+ return func(*xgb.Conn) { restart() }, nil
+ case "kill":
+ return func(conn *xgb.Conn) { killActiveWindow(conn, focused) }, nil
+ case "layout":
+ switch arg {
+ case "tileHorizontal", "tileVertical", "tileWithMaster":
+ return setLayout(arg), nil
+ }
+ return nil, fmt.Errorf("unknown layout %q", arg)
+ case "masters":
+ n, err := strconv.Atoi(arg)
+ if err != nil {
+ return nil, fmt.Errorf("bad master count change %q", arg)
+ }
+ return incMasters(n), nil
+ case "ratio":
+ d, err := strconv.ParseFloat(arg, 64)
+ if err != nil {
+ return nil, fmt.Errorf("bad ratio change %q", arg)
+ }
+ return incRatio(d), nil
+ case "view", "toggle", "tag":
+ var tags BitMask
+ if arg == "all" {
+ tags = BitMask(1)<<tagCount - 1
+ } else {
+ n, err := strconv.Atoi(arg)
+ if err != nil || n < 1 || n > tagCount {
+ return nil, fmt.Errorf("bad tag %q", arg)
+ }
+ tags = BitMask(1) << (n - 1)
+ }
+ switch name {
+ case "view":
+ return func(conn *xgb.Conn) { switchTags(conn, tags) }, nil
+ case "toggle":
+ return func(conn *xgb.Conn) { toggleView(conn, tags) }, nil
+ }
+ return func(conn *xgb.Conn) { moveToTag(conn, tags) }, nil
+ }
+ return nil, fmt.Errorf("unknown action %q", name)
+}