diff options
| author | crecher <crecher@crecher.net> | 2026-10-26 00:19:42 +0000 |
|---|---|---|
| committer | crecher <crecher@crecher.net> | 2026-10-26 00:19:42 +0000 |
| commit | 1db9b13964082042a9578d372723777273c50daa (patch) | |
| tree | 7c60b907298b4d2c70f5e0fedecd608db03aaee8 | |
| parent | 4e257e15b4ddc25ff222613491f7e16354d2aeb3 (diff) | |
| download | wm-1db9b13964082042a9578d372723777273c50daa.tar.gz wm-1db9b13964082042a9578d372723777273c50daa.zip | |
added configuring via .config file!!
Signed-off-by: crecher <crecher@crecher.net>
| -rw-r--r-- | README.md | 12 | ||||
| -rw-r--r-- | config.go | 78 | ||||
| -rw-r--r-- | default_config.toml | 63 | ||||
| -rw-r--r-- | go.mod | 2 | ||||
| -rw-r--r-- | go.sum | 2 | ||||
| -rw-r--r-- | keysym/names.go | 222 | ||||
| -rw-r--r-- | main.go | 418 | ||||
| -rw-r--r-- | xft.go | 2 |
8 files changed, 658 insertions, 141 deletions
@@ -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" @@ -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 @@ -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, +} @@ -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) +} @@ -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; } |