diff options
| -rw-r--r-- | config.go | 16 | ||||
| -rw-r--r-- | main.go | 293 | ||||
| -rwxr-xr-x | start.sh | 4 | ||||
| -rw-r--r-- | xft.go | 131 |
4 files changed, 392 insertions, 52 deletions
@@ -1,8 +1,6 @@ package main import ( - "os" - "github.com/BurntSushi/xgb" "github.com/BurntSushi/xgb/xproto" @@ -12,7 +10,15 @@ import ( // appearance var barHeight uint16 = 20 var gapSize int = 10 -var barColor uint32 = 0x000000 +var barColor uint32 = 0x222222 +var barFg uint32 = 0xBBBBBB +var selBg uint32 = 0x005577 +var selFg uint32 = 0xEEEEEE +var fontName = "JetBrainsMono Nerd Font:size=16" +var tagCount = 9 + +// shows focused window title +var windowTitle = 0 var mod = uint16(xproto.ModMask1) @@ -24,14 +30,14 @@ var keys = []Key{ Mod: mod, Sym: keysym.XK_Return, Fn: func(conn *xgb.Conn) { - spawn("st") + spawn("kitty") }, }, { Mod: mod | xproto.ModMaskShift, Sym: keysym.XK_q, Fn: func(conn *xgb.Conn) { - os.Exit(0) + quit() }, }, { @@ -1,9 +1,12 @@ package main import ( - "fmt" + "bufio" "log" + "os" "os/exec" + "strconv" + "time" "github.com/BurntSushi/xgb" "github.com/BurntSushi/xgb/xproto" @@ -19,7 +22,6 @@ type Key struct { Fn func(*xgb.Conn) } -// TODO rewrite tagging system type Client struct { Win xproto.Window X int16 @@ -41,8 +43,14 @@ var ( // bar var ( - barWindow xproto.Window - barGC xproto.Gcontext + barWindow xproto.Window + barGC xproto.Gcontext + fontID xproto.Font + fontOK bool + fontAscent int + fontDescent int + statusText string + statusChan chan string ) // layout @@ -61,10 +69,33 @@ func main() { } setup(conn) for { - ev, err := conn.WaitForEvent() - if err != nil { - log.Fatal(err) + dirty := false + drain: + for { + select { + case s, ok := <-statusChan: + if !ok { + break drain + } + statusText = s + dirty = true + default: + break drain + } + } + if dirty { + drawBar(conn) } + + ev, xerr := conn.PollForEvent() + if xerr != nil { + log.Fatal(xerr) + } + if ev == nil { + time.Sleep(50 * time.Millisecond) + continue + } + // event loop switch e := ev.(type) { case xproto.KeyPressEvent: @@ -73,28 +104,20 @@ func main() { manageWindow(conn, e.Window) case xproto.DestroyNotifyEvent: unmanageWindow(conn, e.Window) - // case xproto.UnmapNotifyEvent: - // unmanageWindow(conn, e.Window) + case xproto.PropertyNotifyEvent: + if e.Window == focused { + drawBar(conn) + } case xproto.EnterNotifyEvent: // change focused window event focused = e.Event setInputFocus(conn, focused) - // update bar - if barHeight != 0 { - title := getWindowTitle(conn, focused) - drawBarText(conn, fmt.Sprintf("[%s]", title)) - } + drawBar(conn) case xproto.ExposeEvent: if e.Window == barWindow { - title := getWindowTitle(conn, focused) - if title == "None" { - title = "wm" - } - if barHeight != 0 { - drawBarText(conn, fmt.Sprintf("[%s]", title)) - } + drawBar(conn) } default: - log.Println("event: ", e) + // log.Println("event: ", e) } } @@ -118,8 +141,11 @@ func setup(conn *xgb.Conn) { grabKeys(conn) if barHeight != 0 { createBar(conn) + drawBar(conn) } + startStatus() + for _, cmd := range startup { exec.Command(cmd) log.Println("Running startup command: " + cmd) @@ -136,6 +162,32 @@ func spawn(cmd string, args ...string) { } } +func quit() { + os.Exit(0) +} + +func startStatus() { + statusChan = make(chan string, 1) + go func() { + sc := bufio.NewScanner(os.Stdin) + if sc.Err != nil { + log.Println("scanner failed: ", sc.Err().Error) + } + for sc.Scan() { + text := sc.Text() + select { + case statusChan <- text: + default: + select { + case <-statusChan: + default: + } + statusChan <- text + } + } + }() +} + func killActiveWindow(conn *xgb.Conn, win xproto.Window) { if win == 0 { log.Println("no focused window to kill") @@ -283,7 +335,7 @@ func manageWindow(conn *xgb.Conn, win xproto.Window) { clients[win] = client clientList = append([]*Client{client}, clientList...) - log.Println("managing window:", win) + // log.Println("managing window:", win) // listen for events on this window xproto.ChangeWindowAttributes(conn, win, @@ -299,6 +351,7 @@ func manageWindow(conn *xgb.Conn, win xproto.Window) { xproto.MapWindow(conn, win) focused = win setInputFocus(conn, focused) + drawBar(conn) } // xproto.MapWindow(conn, win) @@ -317,7 +370,7 @@ func removeClient(win xproto.Window) { func unmanageWindow(conn *xgb.Conn, win xproto.Window) { if _, ok := clients[win]; ok { - log.Println("unmanaging window:", win) + // log.Println("unmanaging window:", win) removeClient(win) delete(clients, win) updateWindows(conn) @@ -328,12 +381,12 @@ func switchTags(conn *xgb.Conn, tags BitMask) { GlobalTags = tags // unmap everything - for _, client := range clients { xproto.UnmapWindow(conn, client.Win) } // update windows updateWindows(conn) + drawBar(conn) } func tileVertical(conn *xgb.Conn) { @@ -386,7 +439,7 @@ func tileHorizontal(conn *xgb.Conn) { for _, c := range ActiveClients { xOffset := int(i * widthPerWin) width := widthPerWin - gapSize - log.Println(i, len(ActiveClients)) + // log.Println(i, len(ActiveClients)) if i == len(ActiveClients)-1 { width = widthPerWin - gapSize*2 } @@ -455,7 +508,7 @@ func tileWithMaster(conn *xgb.Conn) { yOffset := int(barHeight) + (i * heightPerWin) + gapSize xOffset := uint32(widthPerWin + gapSize) height := heightPerWin - gapSize*2 - log.Println(len(ActiveClients), i) + // log.Println(len(ActiveClients), i) if n > 2 { height = heightPerWin - gapSize } @@ -507,21 +560,90 @@ func createBar(conn *xgb.Conn) { // map bar window xproto.MapWindow(conn, barWindow) + if err := xftInit(fontName); err != nil { + log.Println("xft unavailable, falling back to core font:", err) + } else { + fontAscent, fontDescent = xftMetrics() + fontOK = true + if h := fontAscent + fontDescent; h > 0 && uint16(h) != barHeight { + barHeight = uint16(h) + xproto.ConfigureWindow(conn, barWindow, + xproto.ConfigWindowHeight, []uint32{uint32(barHeight)}) + } + } + + // fallback + if !xftOK { + for _, name := range []string{"fixed"} { + if f, ok := openFont(conn, name); ok { + fontID = f + fontOK = true + break + } + } + } + // create bar gc + masks := uint32(xproto.GcForeground | xproto.GcBackground) + vals := []uint32{barFg, barColor} + if !xftOK && fontOK { + masks |= xproto.GcFont + vals = append(vals, uint32(fontID)) + if r, err := xproto.QueryTextExtents(conn, xproto.Fontable(fontID), asciiChars("M"), 1).Reply(); err == nil && r != nil { + fontAscent = int(r.OverallAscent) + fontDescent = int(r.OverallDescent) + } + } barGC, _ = xproto.NewGcontextId(conn) - xproto.CreateGC(conn, barGC, xproto.Drawable(barWindow), xproto.GcForeground|xproto.GcBackground, []uint32{0xFFFFFFFF, 0xFFFFFFFF}) + xproto.CreateGC(conn, barGC, xproto.Drawable(barWindow), masks, vals) +} + +func openFont(conn *xgb.Conn, name string) (xproto.Font, bool) { + fid, err := xproto.NewFontId(conn) + if err != nil { + return 0, false + } + if err := xproto.OpenFontChecked(conn, fid, uint16(len(name)), name).Check(); err != nil { + return 0, false + } + return fid, true } -func drawBarText(conn *xgb.Conn, text string) { +func textWidth(conn *xgb.Conn, text string) int { if text == "" { - text = " " + return 0 + } + if xftOK { + return xftTextWidth(text) } + if fontOK { + if r, err := xproto.QueryTextExtents(conn, xproto.Fontable(fontID), asciiChars(text), uint16(len(text))).Reply(); err == nil && r != nil { + return int(r.OverallWidth) + } + } + return len(text) * 6 +} - screen := xproto.Setup(conn).DefaultScreen(conn) - xproto.ClearArea(conn, false, barWindow, 0, 0, screen.WidthInPixels, barHeight) - screenWidth := int(screen.WidthInPixels) +func asciiChars(text string) []xproto.Char2b { + chars := make([]xproto.Char2b, len(text)) + for i := 0; i < len(text); i++ { + chars[i] = xproto.Char2b{Byte1: 0, Byte2: text[i]} + } + return chars +} + +func drawBarText(conn *xgb.Conn, x, top, height int, text string, fg, bg uint32) { + if text == "" { + return + } + + baseline := top + (height+fontAscent-fontDescent)/2 + + if xftOK { + xftDrawText(barWindow, x, baseline, text, fg) + return + } - // ascii only because FUCK POLYTEXT8 ascii := make([]byte, 0, len(text)) for i := 0; i < len(text); i++ { if text[i] >= 32 && text[i] <= 126 { @@ -529,46 +651,127 @@ func drawBarText(conn *xgb.Conn, text string) { } } if len(ascii) == 0 { - ascii = []byte(" ") + return } textItem := make([]byte, len(ascii)+2) textItem[0] = byte(len(ascii)) // string len - textItem[1] = 0 // offset copy(textItem[2:], ascii) - err := xproto.PolyText8Checked(conn, xproto.Drawable(barWindow), barGC, int16(screenWidth/2-len(ascii)*2), int16(barHeight-5), textItem).Check() - if err != nil { - log.Println("polytext failed again :D err:", err) + xproto.ChangeGC(conn, barGC, xproto.GcForeground|xproto.GcBackground, []uint32{fg, bg}) + xproto.PolyText8(conn, xproto.Drawable(barWindow), barGC, int16(x), int16(baseline), textItem) +} + +func drawBar(conn *xgb.Conn) { + if barWindow == 0 || barHeight == 0 { + return + } + screen := xproto.Setup(conn).DefaultScreen(conn) + barWidth := int(screen.WidthInPixels) + + xproto.ClearArea(conn, false, barWindow, 0, 0, screen.WidthInPixels, barHeight) + + stw := 0 + if statusText != "" { + stw = textWidth(conn, statusText) + 2 // 2px right padding like dwl + drawBarText(conn, barWidth-stw, 0, int(barHeight), statusText, barFg, barColor) + } + + var occ, focc BitMask + if c, ok := clients[focused]; ok { + focc = c.Tags + } + for _, c := range clients { + occ |= c.Tags + } + + boxs := int(barHeight) / 9 + boxw := int(barHeight)/6 + 2 + + x := 0 + for i := 0; i < tagCount; i++ { + var mask BitMask = 1 << uint(i) + digit := strconv.Itoa(i + 1) + w := textWidth(conn, digit) + int(barHeight) + sel := (GlobalTags & mask) != 0 + fg, bg := barFg, barColor + if sel { + fg, bg = selFg, selBg + rect := xproto.Rectangle{X: int16(x), Y: 0, Width: uint16(w), Height: barHeight} + xproto.ChangeGC(conn, barGC, xproto.GcForeground|xproto.GcBackground, []uint32{selBg, selBg}) + xproto.PolyFillRectangle(conn, xproto.Drawable(barWindow), barGC, []xproto.Rectangle{rect}) + } + drawBarText(conn, x+int(barHeight)/2, 0, int(barHeight), digit, fg, bg) + if occ&mask != 0 { + rect := xproto.Rectangle{X: int16(x + boxs), Y: int16(boxs), Width: uint16(boxw), Height: uint16(boxw)} + if focc&mask != 0 { + xproto.ChangeGC(conn, barGC, xproto.GcForeground|xproto.GcBackground, []uint32{fg, fg}) + xproto.PolyFillRectangle(conn, xproto.Drawable(barWindow), barGC, []xproto.Rectangle{rect}) + } else { + xproto.ChangeGC(conn, barGC, xproto.GcForeground|xproto.GcBackground, []uint32{fg, bg}) + xproto.PolyRectangle(conn, xproto.Drawable(barWindow), barGC, []xproto.Rectangle{rect}) + } + } + x += w + } + + if windowTitle != 0 { + mw := barWidth - stw - x + if mw > int(barHeight) { + title := getWindowTitle(conn, focused) + if title == "None" || title == "Unknown" { + title = "wm" + } + fg, bg := barFg, barColor + if _, ok := clients[focused]; ok { + fg, bg = selFg, selBg + } + drawBarText(conn, x, 0, int(barHeight), title, fg, bg) + } } } func getWindowTitle(conn *xgb.Conn, win xproto.Window) string { - if focused == 0 { + if win == 0 { return "None" } - prop, err := xproto.GetProperty(conn, false, focused, xproto.AtomWmName, xproto.AtomString, 0, 100).Reply() + netWmName, err := xproto.InternAtom(conn, true, uint16(len("_NET_WM_NAME")), "_NET_WM_NAME").Reply() + if err == nil && netWmName != nil && netWmName.Atom != 0 { + prop, err := xproto.GetProperty(conn, false, win, netWmName.Atom, xproto.AtomAny, 0, 100).Reply() + if err == nil && prop != nil && len(prop.Value) > 0 && prop.Value[0] != 0 { + return trimTitle(prop.Value) + } + } + + prop, err := xproto.GetProperty(conn, false, win, xproto.AtomWmName, xproto.AtomAny, 0, 100).Reply() if err != nil { log.Println("failed to get window title:", err) return "Unknown" } - if prop == nil || len(prop.Value) == 0 { + if prop == nil || len(prop.Value) == 0 || prop.Value[0] == 0 { return "Unknown" } - return string(prop.Value) + return trimTitle(prop.Value) +} + +func trimTitle(value []byte) string { + for len(value) > 0 && value[len(value)-1] == 0 { + value = value[:len(value)-1] + } + return string(value) } func updateWindows(conn *xgb.Conn) { ActiveClients = ActiveClients[:0] for _, client := range clientList { - log.Println(client.Tags & GlobalTags) + // log.Println(client.Tags & GlobalTags) if (client.Tags & GlobalTags) >= 1 { xproto.MapWindow(conn, client.Win) ActiveClients = append(ActiveClients, client) - log.Println("Mapping window", client.Win) + // log.Println("Mapping window", client.Win) } } @@ -1,5 +1,5 @@ #!/bin/bash -Xephyr -screen 1280x800 :2 & +Xephyr -screen 1280x800 :3 & go build -DISPLAY=:2 ./wm +date | DISPLAY=:3 ./wm pkill Xephyr @@ -0,0 +1,131 @@ +package main + +/* +#cgo pkg-config: xft xrender x11 fontconfig +#include <stdlib.h> +#include <X11/Xlib.h> +#include <X11/extensions/Xrender.h> +#include <X11/Xft/Xft.h> + +static Display *_dpy; +static XftDraw *_drw; +static XftFont *_ft; +static Visual *_vis; +static Colormap _cmap; +static XftColor _fg; +static int _scr; + +static int wm_xft_init(const char *pattern) +{ + int event_basep, error_basep; + _dpy = XOpenDisplay(NULL); + if (!_dpy) + return -1; + if (!XRenderQueryExtension(_dpy, &event_basep, &error_basep)) + return -3; + XftInit(NULL); + _scr = DefaultScreen(_dpy); + _vis = DefaultVisual(_dpy, _scr); + _cmap = DefaultColormap(_dpy, _scr); + _ft = XftFontOpenName(_dpy, _scr, pattern); + if (!_ft) + return -2; + return 0; +} + +static void wm_xft_setfg(unsigned int px) +{ + XRenderColor c; + c.red = ((px >> 16) & 0xff) << 8; + c.green = ((px >> 8) & 0xff) << 8; + c.blue = (px & 0xff) << 8; + c.alpha = 0xffff; + XftColorAllocValue(_dpy, _vis, _cmap, &c, &_fg); +} + +static int wm_xft_width(const char *s, int n) +{ + XGlyphInfo gi; + XftTextExtentsUtf8(_dpy, _ft, (const FcChar8 *)s, n, &gi); + return gi.xOff; +} + +static void wm_xft_draw(unsigned int win, int x, int y, + const char *s, int n, unsigned int fg) +{ + if (!_dpy || !_ft || !win) + return; + if (!_drw) + _drw = XftDrawCreate(_dpy, (Drawable)win, _vis, _cmap); + if (!_drw) + return; + XftDrawChange(_drw, (Drawable)win); + wm_xft_setfg(fg); + XftDrawStringUtf8(_drw, &_fg, _ft, x, y, (const FcChar8 *)s, n); + XFlush(_dpy); +} + +static int wm_xft_ascent(void) { return _ft ? _ft->ascent : 0; } +static int wm_xft_descent(void) { return _ft ? _ft->descent : 0; } +*/ +import "C" + +import ( + "unsafe" + + "github.com/BurntSushi/xgb/xproto" +) + +var xftOK bool + +func xftInit(pattern string) error { + cp := C.CString(pattern) + defer C.free(unsafe.Pointer(cp)) + + switch rc := int(C.wm_xft_init(cp)); rc { + case 0: + xftOK = true + return nil + case -1: + return errXftNoDisplay + case -2: + return errXftNoFont + default: + return errXftNoRender + } +} + +var ( + errXftNoDisplay = errXft("xft: cannot open display") + errXftNoFont = errXft("xft: cannot load font pattern") + errXftNoRender = errXft("xft: no RENDER extension") +) + +type errXft string + +func (e errXft) Error() string { return string(e) } + +func xftTextWidth(s string) int { + if !xftOK { + return 0 + } + cs := C.CString(s) + defer C.free(unsafe.Pointer(cs)) + return int(C.wm_xft_width(cs, C.int(len(s)))) +} + +func xftDrawText(win xproto.Window, x, baseline int, s string, fg uint32) { + if !xftOK || win == 0 { + return + } + cs := C.CString(s) + defer C.free(unsafe.Pointer(cs)) + C.wm_xft_draw(C.uint(win), C.int(x), C.int(baseline), cs, C.int(len(s)), C.uint(fg)) +} + +func xftMetrics() (ascent, descent int) { + if !xftOK { + return 0, 0 + } + return int(C.wm_xft_ascent()), int(C.wm_xft_descent()) +} |