package main import ( "bufio" "log" "os" "os/exec" "strconv" "time" "github.com/BurntSushi/xgb" "github.com/BurntSushi/xgb/xproto" ) // types type BitMask uint32 // structs type Key struct { Mod uint16 Sym uint32 Fn func(*xgb.Conn) } type Client struct { Win xproto.Window X int16 Y int16 Width uint16 Height uint16 Floating bool Tags BitMask Next *Client Previous *Client MasterTags BitMask } var ( clients = make(map[xproto.Window]*Client) clientList []*Client focused xproto.Window ) // bar var ( barWindow xproto.Window barGC xproto.Gcontext fontID xproto.Font fontOK bool fontAscent int fontDescent int statusText string statusChan chan string ) // layout var layout string = "tileWithMaster" // tags var ( GlobalTags BitMask = 1 ActiveClients []*Client ) func main() { conn, err := xgb.NewConn() if err != nil { log.Fatal(err) } setup(conn) for { 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: handleKeyPress(conn, e) case xproto.MapRequestEvent: manageWindow(conn, e.Window) case xproto.DestroyNotifyEvent: 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) drawBar(conn) case xproto.ExposeEvent: if e.Window == barWindow { drawBar(conn) } default: // log.Println("event: ", e) } } } func setup(conn *xgb.Conn) { root := xproto.Setup(conn).DefaultScreen(conn).Root err := xproto.ChangeWindowAttributesChecked( conn, root, xproto.CwEventMask, []uint32{ xproto.EventMaskSubstructureRedirect | xproto.EventMaskSubstructureNotify, }, ).Check() if err != nil { log.Fatal(err) } grabKeys(conn) if barHeight != 0 { createBar(conn) drawBar(conn) } startStatus() for _, cmd := range startup { exec.Command(cmd) log.Println("Running startup command: " + cmd) } } func spawn(cmd string, args ...string) { c := exec.Command(cmd, args...) c.Stdout = nil c.Stderr = nil c.Stdin = nil if err := c.Start(); err != nil { log.Println("failed to spawn", cmd, ":", err) } } 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") return } // try to use WM_DELETE_WINDOW to close wmProtocols, err := xproto.InternAtom(conn, true, uint16(len("WM_PROTOCOLS")), "WM_PROTOCOLS").Reply() if err != nil { log.Println("failed to get WM_PROTOCOLS atom", err) return } wmDelete, err := xproto.InternAtom(conn, true, uint16(len("WM_DELETE_WINDOW")), "WM_DELETE_WINDOW").Reply() if err != nil { log.Println("failed to get WM_DELETE_WINDOW atom", err) return } data := xproto.ClientMessageDataUnionData32New([]uint32{ uint32(wmDelete.Atom), uint32(xproto.TimeCurrentTime), 0, 0, 0, }) ev := xproto.ClientMessageEvent{ Format: 32, Window: win, Type: wmProtocols.Atom, Data: data, } // send event err = xproto.SendEventChecked(conn, false, win, xproto.EventMaskNoEvent, string(ev.Bytes())).Check() if err != nil { // force close if WM_DELETE_WINDOW doesnt work log.Println("force closing:", err) xproto.DestroyWindow(conn, win) } removeClient(win) delete(clients, win) focused = 0 updateWindows(conn) } func grabKeys(conn *xgb.Conn) { root := xproto.Setup(conn).DefaultScreen(conn).Root for _, k := range keys { keycode := keysymToKeycode(conn, k.Sym) if keycode == 0 { continue } ignoredMods := []uint16{ 0, xproto.ModMaskLock, xproto.ModMask2, xproto.ModMaskLock | xproto.ModMask2, } for _, m := range ignoredMods { xproto.GrabKey(conn, true, root, k.Mod|m, keycode, xproto.GrabModeAsync, xproto.GrabModeAsync, ) } } } func keysymToKeycode(conn *xgb.Conn, sym uint32) xproto.Keycode { setup := xproto.Setup(conn) min := setup.MinKeycode max := setup.MaxKeycode reply, err := xproto.GetKeyboardMapping(conn, min, byte(max-min+1)).Reply() if err != nil { return 0 } keysymsPerKeycode := int(reply.KeysymsPerKeycode) for kc := min; kc <= max; kc++ { for i := 0; i < keysymsPerKeycode; i++ { idx := int(kc-min)*keysymsPerKeycode + i if uint32(reply.Keysyms[idx]) == sym { return kc } } } return 0 } func handleKeyPress(conn *xgb.Conn, e xproto.KeyPressEvent) { reply, err := xproto.GetKeyboardMapping(conn, e.Detail, 1).Reply() if err != nil || len(reply.Keysyms) == 0 { return } sym := reply.Keysyms[0] for _, k := range keys { if uint32(sym) != k.Sym { continue } ignoredMods := uint16(xproto.ModMaskLock) | uint16(xproto.ModMask2) modifiers := e.State &^ ignoredMods if modifiers == k.Mod { k.Fn(conn) } } } func setInputFocus(conn *xgb.Conn, win xproto.Window) { if win == 0 { return } xproto.SetInputFocus(conn, xproto.InputFocusPointerRoot, win, xproto.TimeCurrentTime) } func manageWindow(conn *xgb.Conn, win xproto.Window) { // TODO: add window rules geom, err := xproto.GetGeometry(conn, xproto.Drawable(win)).Reply() if err != nil { return } client := &Client{ Win: win, X: geom.X, Y: geom.Y, Width: geom.Width, Height: geom.Height, Floating: false, Tags: GlobalTags, } clients[win] = client clientList = append([]*Client{client}, clientList...) // log.Println("managing window:", win) // listen for events on this window xproto.ChangeWindowAttributes(conn, win, xproto.CwEventMask, []uint32{ xproto.EventMaskEnterWindow | xproto.EventMaskFocusChange | xproto.EventMaskPropertyChange, }) // map (show) window if (client.Tags & GlobalTags) >= 1 { xproto.MapWindow(conn, win) focused = win setInputFocus(conn, focused) drawBar(conn) } // xproto.MapWindow(conn, win) // tile all windows updateWindows(conn) } func removeClient(win xproto.Window) { for i, c := range clientList { if c.Win == win { clientList = append(clientList[:i], clientList[i+1:]...) return } } } func unmanageWindow(conn *xgb.Conn, win xproto.Window) { if _, ok := clients[win]; ok { // log.Println("unmanaging window:", win) removeClient(win) delete(clients, win) updateWindows(conn) } } 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) { screen := xproto.Setup(conn).DefaultScreen(conn) screenWidth := int(screen.WidthInPixels) usableHeight := int(screen.HeightInPixels) - int(barHeight) n := len(ActiveClients) if n == 0 { return } heightPerWin := usableHeight / n i := 0 for _, c := range ActiveClients { yOffset := int(barHeight) + (i * heightPerWin) height := heightPerWin - gapSize if i == len(ActiveClients)-1 { height = heightPerWin - gapSize*2 } xproto.ConfigureWindow(conn, c.Win, xproto.ConfigWindowX| xproto.ConfigWindowY| xproto.ConfigWindowWidth| xproto.ConfigWindowHeight, []uint32{ 0 + uint32(gapSize), // x uint32(yOffset + gapSize), // y uint32(screenWidth - gapSize*2), uint32(height), }) i++ } } func tileHorizontal(conn *xgb.Conn) { screen := xproto.Setup(conn).DefaultScreen(conn) screenWidth := int(screen.WidthInPixels) screenHeight := int(screen.HeightInPixels) n := len(ActiveClients) if n == 0 { return } widthPerWin := screenWidth / n i := 0 for _, c := range ActiveClients { xOffset := int(i * widthPerWin) width := widthPerWin - gapSize // log.Println(i, len(ActiveClients)) if i == len(ActiveClients)-1 { width = widthPerWin - gapSize*2 } xproto.ConfigureWindow(conn, c.Win, xproto.ConfigWindowX| xproto.ConfigWindowY| xproto.ConfigWindowWidth| xproto.ConfigWindowHeight, []uint32{ uint32(xOffset + gapSize), // x uint32(int(barHeight) + gapSize), // y uint32(width), uint32(screenHeight - int(barHeight) - gapSize*2), }) i++ } } func tileWithMaster(conn *xgb.Conn) { screen := xproto.Setup(conn).DefaultScreen(conn) screenWidth := int(screen.WidthInPixels) 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 } 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(int(barHeight)), // y uint32(screenWidth), // width uint32(screenHeight - int(barHeight)), }) } else { xproto.ConfigureWindow(conn, c.Win, xproto.ConfigWindowX| xproto.ConfigWindowY| xproto.ConfigWindowWidth| xproto.ConfigWindowHeight, []uint32{ uint32(0 + gapSize), // x uint32(int(barHeight) + gapSize), // y uint32(screenWidth/2 - int(gapSize)), // width uint32(screenHeight - int(barHeight) - gapSize*2), }) } } else { heightPerWin := usableHeight / (n - 1) yOffset := int(barHeight) + (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 uint32(widthPerWin - gapSize*2), uint32(height), }) i++ } } } func createBar(conn *xgb.Conn) { screen := xproto.Setup(conn).DefaultScreen(conn) root := screen.Root // create the window cw := []uint32{ barColor, // should be black background xproto.EventMaskExposure, // redraw on expose } barWindow, _ = xproto.NewWindowId(conn) xproto.CreateWindow( conn, screen.RootDepth, barWindow, root, 0, 0, // x, y screen.WidthInPixels, barHeight, 0, // border xproto.WindowClassInputOutput, screen.RootVisual, xproto.CwBackPixel|xproto.CwEventMask, cw, ) // 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), 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 textWidth(conn *xgb.Conn, text string) int { if 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 } 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 := make([]byte, 0, len(text)) for i := 0; i < len(text); i++ { if text[i] >= 32 && text[i] <= 126 { ascii = append(ascii, text[i]) } } if len(ascii) == 0 { return } textItem := make([]byte, len(ascii)+2) textItem[0] = byte(len(ascii)) // string len 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) } 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 win == 0 { return "None" } 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 || prop.Value[0] == 0 { return "Unknown" } 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) if (client.Tags & GlobalTags) >= 1 { xproto.MapWindow(conn, client.Win) ActiveClients = append(ActiveClients, client) // log.Println("Mapping window", client.Win) } } switch layout { case "tileHorizontal": tileHorizontal(conn) case "tileVertical": tileVertical(conn) case "tileWithMaster": tileWithMaster(conn) default: log.Fatal("Unknown layout ", layout) } }