diff options
Diffstat (limited to 'main.go')
| -rw-r--r-- | main.go | 418 |
1 files changed, 356 insertions, 62 deletions
@@ -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) +} |