aboutsummaryrefslogtreecommitdiff
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
parent4e257e15b4ddc25ff222613491f7e16354d2aeb3 (diff)
downloadwm-1db9b13964082042a9578d372723777273c50daa.tar.gz
wm-1db9b13964082042a9578d372723777273c50daa.zip
added configuring via .config file!!
Signed-off-by: crecher <crecher@crecher.net>
-rw-r--r--README.md12
-rw-r--r--config.go78
-rw-r--r--default_config.toml63
-rw-r--r--go.mod2
-rw-r--r--go.sum2
-rw-r--r--keysym/names.go222
-rw-r--r--main.go418
-rw-r--r--xft.go2
8 files changed, 658 insertions, 141 deletions
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
+`"<mods>+<key>" = "<action>"`, where `mod` is your configured mod key and an empty action unbinds.
+actions: `spawn <shell command>`, `quit`, `restart`, `kill`, `layout <name>`,
+`masters <+n|-n>`, `ratio <+0.05|-0.05>`, `view <n|all>`, `toggle <n|all>`, `tag <n|all>`.
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)<<tagCount - 1
- keys = append(keys,
- bind(mod, keysym.XK_0, func(conn *xgb.Conn) { switchTags(conn, all) }),
- bind(mod|shift, keysym.XK_0, func(conn *xgb.Conn) { moveToTag(conn, all) }),
- )
-}
diff --git a/default_config.toml b/default_config.toml
new file mode 100644
index 0000000..1557d0b
--- /dev/null
+++ b/default_config.toml
@@ -0,0 +1,63 @@
+gap = 10
+border_width = 2
+bar_height = 20
+font = "JetBrainsMono Nerd Font:size=16"
+tags = 9
+layout = "tileWithMaster"
+master_count = 1
+master_ratio = 0.5
+mod = "super"
+window_title = false
+startup = ["notify-send hello world!"]
+
+[colors]
+bar_bg = "#000000"
+bar_fg = "#888888"
+sel_bg = "#285577"
+sel_fg = "#EEEEEE"
+border = "#444444"
+border_focus = "#285577"
+
+[binds]
+"mod+Return" = "spawn st"
+"mod+w" = "spawn qutebrowser"
+"mod+d" = "spawn dmenu_run"
+"mod+shift+q" = "quit"
+"mod+shift+r" = "restart"
+"mod+q" = "kill"
+"mod+l" = "layout tileHorizontal"
+"mod+shift+l" = "layout tileVertical"
+"mod+m" = "layout tileWithMaster"
+"mod+i" = "masters +1"
+"mod+shift+i" = "masters -1"
+"mod+shift+h" = "ratio -0.05"
+"mod+shift+j" = "ratio +0.05"
+"mod+0" = "view all"
+"mod+shift+0" = "tag all"
+"mod+1" = "view 1"
+"mod+shift+1" = "tag 1"
+"alt+1" = "toggle 1"
+"mod+2" = "view 2"
+"mod+shift+2" = "tag 2"
+"alt+2" = "toggle 2"
+"mod+3" = "view 3"
+"mod+shift+3" = "tag 3"
+"alt+3" = "toggle 3"
+"mod+4" = "view 4"
+"mod+shift+4" = "tag 4"
+"alt+4" = "toggle 4"
+"mod+5" = "view 5"
+"mod+shift+5" = "tag 5"
+"alt+5" = "toggle 5"
+"mod+6" = "view 6"
+"mod+shift+6" = "tag 6"
+"alt+6" = "toggle 6"
+"mod+7" = "view 7"
+"mod+shift+7" = "tag 7"
+"alt+7" = "toggle 7"
+"mod+8" = "view 8"
+"mod+shift+8" = "tag 8"
+"alt+8" = "toggle 8"
+"mod+9" = "view 9"
+"mod+shift+9" = "tag 9"
+"alt+9" = "toggle 9"
diff --git a/go.mod b/go.mod
index 8584433..20eae24 100644
--- a/go.mod
+++ b/go.mod
@@ -3,3 +3,5 @@ module github.com/crecheer/wm
go 1.25.0
require github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc
+
+require github.com/BurntSushi/toml v1.4.0 // indirect
diff --git a/go.sum b/go.sum
index ae50bbf..9425060 100644
--- a/go.sum
+++ b/go.sum
@@ -1,2 +1,4 @@
+github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
+github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc h1:7D+Bh06CRPCJO3gr2F7h1sriovOZ8BMhca2Rg85c2nk=
github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
diff --git a/keysym/names.go b/keysym/names.go
new file mode 100644
index 0000000..1f1fc3b
--- /dev/null
+++ b/keysym/names.go
@@ -0,0 +1,222 @@
+package keysym
+
+var Names = map[string]uint32{
+ "BackSpace": 0xff08,
+ "Tab": 0xff09,
+ "Linefeed": 0xff0a,
+ "Clear": 0xff0b,
+ "Return": 0xff0d,
+ "Pause": 0xff13,
+ "Scroll_Lock": 0xff14,
+ "Sys_Req": 0xff15,
+ "Escape": 0xff1b,
+ "Delete": 0xffff,
+ "space": 0x0020,
+ "exclam": 0x0021,
+ "quotedbl": 0x0022,
+ "numbersign": 0x0023,
+ "dollar": 0x0024,
+ "percent": 0x0025,
+ "ampersand": 0x0026,
+ "apostrophe": 0x0027,
+ "quoteright": 0x0027,
+ "parenleft": 0x0028,
+ "parenright": 0x0029,
+ "asterisk": 0x002a,
+ "plus": 0x002b,
+ "comma": 0x002c,
+ "minus": 0x002d,
+ "period": 0x002e,
+ "slash": 0x002f,
+ "0": 0x0030,
+ "1": 0x0031,
+ "2": 0x0032,
+ "3": 0x0033,
+ "4": 0x0034,
+ "5": 0x0035,
+ "6": 0x0036,
+ "7": 0x0037,
+ "8": 0x0038,
+ "9": 0x0039,
+ "colon": 0x003a,
+ "semicolon": 0x003b,
+ "less": 0x003c,
+ "equal": 0x003d,
+ "greater": 0x003e,
+ "question": 0x003f,
+ "at": 0x0040,
+ "A": 0x0041,
+ "B": 0x0042,
+ "C": 0x0043,
+ "D": 0x0044,
+ "E": 0x0045,
+ "F": 0x0046,
+ "G": 0x0047,
+ "H": 0x0048,
+ "I": 0x0049,
+ "J": 0x004a,
+ "K": 0x004b,
+ "L": 0x004c,
+ "M": 0x004d,
+ "N": 0x004e,
+ "O": 0x004f,
+ "P": 0x0050,
+ "Q": 0x0051,
+ "R": 0x0052,
+ "S": 0x0053,
+ "T": 0x0054,
+ "U": 0x0055,
+ "V": 0x0056,
+ "W": 0x0057,
+ "X": 0x0058,
+ "Y": 0x0059,
+ "Z": 0x005a,
+ "bracketleft": 0x005b,
+ "backslash": 0x005c,
+ "bracketright": 0x005d,
+ "asciicircum": 0x005e,
+ "underscore": 0x005f,
+ "grave": 0x0060,
+ "quoteleft": 0x0060,
+ "a": 0x0061,
+ "b": 0x0062,
+ "c": 0x0063,
+ "d": 0x0064,
+ "e": 0x0065,
+ "f": 0x0066,
+ "g": 0x0067,
+ "h": 0x0068,
+ "i": 0x0069,
+ "j": 0x006a,
+ "k": 0x006b,
+ "l": 0x006c,
+ "m": 0x006d,
+ "n": 0x006e,
+ "o": 0x006f,
+ "p": 0x0070,
+ "q": 0x0071,
+ "r": 0x0072,
+ "s": 0x0073,
+ "t": 0x0074,
+ "u": 0x0075,
+ "v": 0x0076,
+ "w": 0x0077,
+ "x": 0x0078,
+ "y": 0x0079,
+ "z": 0x007a,
+ "braceleft": 0x007b,
+ "bar": 0x007c,
+ "braceright": 0x007d,
+ "asciitilde": 0x007e,
+ "nobreakspace": 0x00a0,
+ "exclamdown": 0x00a1,
+ "cent": 0x00a2,
+ "sterling": 0x00a3,
+ "currency": 0x00a4,
+ "yen": 0x00a5,
+ "brokenbar": 0x00a6,
+ "section": 0x00a7,
+ "diaeresis": 0x00a8,
+ "copyright": 0x00a9,
+ "ordfeminine": 0x00aa,
+ "guillemotleft": 0x00ab,
+ "notsign": 0x00ac,
+ "hyphen": 0x00ad,
+ "registered": 0x00ae,
+ "macron": 0x00af,
+ "degree": 0x00b0,
+ "plusminus": 0x00b1,
+ "twosuperior": 0x00b2,
+ "threesuperior": 0x00b3,
+ "acute": 0x00b4,
+ "mu": 0x00b5,
+ "paragraph": 0x00b6,
+ "periodcentered": 0x00b7,
+ "cedilla": 0x00b8,
+ "onesuperior": 0x00b9,
+ "masculine": 0x00ba,
+ "guillemotright": 0x00bb,
+ "onequarter": 0x00bc,
+ "onehalf": 0x00bd,
+ "threequarters": 0x00be,
+ "questiondown": 0x00bf,
+ "Agrave": 0x00c0,
+ "Aacute": 0x00c1,
+ "Acircumflex": 0x00c2,
+ "Atilde": 0x00c3,
+ "Adiaeresis": 0x00c4,
+ "Aring": 0x00c5,
+ "AE": 0x00c6,
+ "Ccedilla": 0x00c7,
+ "Egrave": 0x00c8,
+ "Eacute": 0x00c9,
+ "Ecircumflex": 0x00ca,
+ "Ediaeresis": 0x00cb,
+ "Igrave": 0x00cc,
+ "Iacute": 0x00cd,
+ "Icircumflex": 0x00ce,
+ "Idiaeresis": 0x00cf,
+ "ETH": 0x00d0,
+ "Eth": 0x00d0,
+ "Ntilde": 0x00d1,
+ "Ograve": 0x00d2,
+ "Oacute": 0x00d3,
+ "Ocircumflex": 0x00d4,
+ "Otilde": 0x00d5,
+ "Odiaeresis": 0x00d6,
+ "multiply": 0x00d7,
+ "Oslash": 0x00d8,
+ "Ooblique": 0x00d8,
+ "Ugrave": 0x00d9,
+ "Uacute": 0x00da,
+ "Ucircumflex": 0x00db,
+ "Udiaeresis": 0x00dc,
+ "Yacute": 0x00dd,
+ "THORN": 0x00de,
+ "Thorn": 0x00de,
+ "ssharp": 0x00df,
+ "agrave": 0x00e0,
+ "aacute": 0x00e1,
+ "acircumflex": 0x00e2,
+ "atilde": 0x00e3,
+ "adiaeresis": 0x00e4,
+ "aring": 0x00e5,
+ "ae": 0x00e6,
+ "ccedilla": 0x00e7,
+ "egrave": 0x00e8,
+ "eacute": 0x00e9,
+ "ecircumflex": 0x00ea,
+ "ediaeresis": 0x00eb,
+ "igrave": 0x00ec,
+ "iacute": 0x00ed,
+ "icircumflex": 0x00ee,
+ "idiaeresis": 0x00ef,
+ "eth": 0x00f0,
+ "ntilde": 0x00f1,
+ "ograve": 0x00f2,
+ "oacute": 0x00f3,
+ "ocircumflex": 0x00f4,
+ "otilde": 0x00f5,
+ "odiaeresis": 0x00f6,
+ "division": 0x00f7,
+ "oslash": 0x00f8,
+ "ooblique": 0x00f8,
+ "ugrave": 0x00f9,
+ "uacute": 0x00fa,
+ "ucircumflex": 0x00fb,
+ "udiaeresis": 0x00fc,
+ "yacute": 0x00fd,
+ "thorn": 0x00fe,
+ "ydiaeresis": 0x00ff,
+ "Home": 0xff50,
+ "Left": 0xff51,
+ "Up": 0xff52,
+ "Right": 0xff53,
+ "Down": 0xff54,
+ "Prior": 0xff55,
+ "Page_Up": 0xff55,
+ "Next": 0xff56,
+ "Page_Down": 0xff56,
+ "End": 0xff57,
+ "Begin": 0xff58,
+}
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)
+}
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; }