From 1db9b13964082042a9578d372723777273c50daa Mon Sep 17 00:00:00 2001 From: crecher Date: Mon, 26 Oct 2026 00:19:42 +0000 Subject: added configuring via .config file!! Signed-off-by: crecher --- README.md | 12 ++ config.go | 78 ---------- default_config.toml | 63 ++++++++ go.mod | 2 + go.sum | 2 + keysym/names.go | 222 ++++++++++++++++++++++++++++ main.go | 418 ++++++++++++++++++++++++++++++++++++++++++++-------- xft.go | 2 +- 8 files changed, 658 insertions(+), 141 deletions(-) delete mode 100644 config.go create mode 100644 default_config.toml create mode 100644 keysym/names.go diff --git a/README.md b/README.md index 58d8865..2173415 100644 --- a/README.md +++ b/README.md @@ -14,3 +14,15 @@ exit it with master+shift+q ```sh ./start.sh ``` + +## config + +config location is `XDG_CONFIG_HOME/wm/config.toml`. On startup, if the wm doesn't find +a file at that location, it will copy `default_config.toml` there. it's read on startup, +so to apply changes you can use the restart keybind (`mod+shift+r` by default), any keys +left out will use the defaults, invalid values are logged and skipped. + +binds are in `[binds]` as +`"+" = ""`, where `mod` is your configured mod key and an empty action unbinds. +actions: `spawn `, `quit`, `restart`, `kill`, `layout `, +`masters <+n|-n>`, `ratio <+0.05|-0.05>`, `view `, `toggle `, `tag `. diff --git a/config.go b/config.go deleted file mode 100644 index 8cab5d8..0000000 --- a/config.go +++ /dev/null @@ -1,78 +0,0 @@ -package main - -import ( - "github.com/BurntSushi/xgb" - "github.com/BurntSushi/xgb/xproto" - - "github.com/crecheer/wm/keysym" -) - -// appearance -var barHeight uint16 = 20 -var gapSize int = 10 -var barColor uint32 = 0x000000 -var barFg uint32 = 0x888888 -var selBg uint32 = 0x285577 -var selFg uint32 = 0xEEEEEE -var fontName = "JetBrainsMono Nerd Font:size=16" -var tagCount = 9 -var borderWidth int = 2 -var borderColor uint32 = 0x444444 -var borderFocus uint32 = 0x285577 - -// shows focused window title -var windowTitle = 0 - -// xproto.ModMask1 = Alt -// xproto.ModMask4 = Super / Win -var mod = uint16(xproto.ModMask4) - -// commands that get executed on startup -var startup = []string{"notify-send hello world!"} - -const shift = uint16(xproto.ModMaskShift) -const alt = uint16(xproto.ModMask1) - -func bind(m uint16, sym uint32, fn func(*xgb.Conn)) Key { - return Key{Mod: m, Sym: sym, Fn: fn} -} - -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) - } -} - -var keys = []Key{ - bind(mod, keysym.XK_Return, run("st")), - bind(mod, keysym.XK_w, run("qutebrowser")), - bind(mod, keysym.XK_d, run("dmenu_run")), - bind(mod|shift, keysym.XK_q, func(*xgb.Conn) { quit() }), - bind(mod|shift, keysym.XK_r, func(*xgb.Conn) { restart() }), - bind(mod, keysym.XK_q, func(conn *xgb.Conn) { killActiveWindow(conn, focused) }), - bind(mod, keysym.XK_l, setLayout("tileHorizontal")), - bind(mod|shift, keysym.XK_l, setLayout("tileVertical")), - bind(mod, keysym.XK_m, setLayout("tileWithMaster")), -} - -func init() { - for i := 0; i < tagCount && i < 9; i++ { - tags := BitMask(1) << i - sym := keysym.XK_1 + uint32(i) - keys = append(keys, - bind(mod, sym, func(conn *xgb.Conn) { switchTags(conn, tags) }), - bind(mod|shift, sym, func(conn *xgb.Conn) { moveToTag(conn, tags) }), - bind(alt, sym, func(conn *xgb.Conn) { toggleView(conn, tags) }), - ) - } - all := BitMask(1)< 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 { + 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) +} diff --git a/xft.go b/xft.go index 73ab52b..d1e4e85 100644 --- a/xft.go +++ b/xft.go @@ -62,7 +62,7 @@ static void wm_xft_draw(unsigned int win, int x, int y, XftDrawChange(_drw, (Drawable)win); wm_xft_setfg(fg); XftDrawStringUtf8(_drw, &_fg, _ft, x, y, (const FcChar8 *)s, n); - XFlush(_dpy); + XSync(_dpy, False); } static int wm_xft_ascent(void) { return _ft ? _ft->ascent : 0; } -- cgit v1.2.3