package main import ( "bufio" "log" "os" "os/exec" "strconv" "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) events := make(chan xgb.Event) go func() { for { ev, xerr := conn.WaitForEvent() if ev == nil && xerr == nil { close(events) return } if xerr != nil { log.Println("x error:", xerr) } if ev != nil { events <- ev } } }() for { select { case s := <-statusChan: statusText = s drawBar(conn) case ev, ok := <-events: if !ok { log.Fatal("lost connection to X server") } handleEvent(conn, ev) } } } func handleEvent(conn *xgb.Conn, ev xgb.Event) { 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() if restoreState(conn) { return } for _, cmd := range startup { log.Println("Running startup command: " + cmd) spawn("sh", "-c", 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) defer func() { if err := sc.Err(); err != nil { log.Println("scanner failed:", err) } }() 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 _, ok := clients[win]; !ok { log.Println("no focused window to kill") return } if supportsDeleteWindow(conn, win) { if err := sendDeleteWindow(conn, win); err != nil { log.Println("failed to send WM_DELETE_WINDOW:", err) } return } xproto.KillClient(conn, uint32(win)) } func internAtom(conn *xgb.Conn, name string) (xproto.Atom, error) { reply, err := xproto.InternAtom(conn, false, uint16(len(name)), name).Reply() if err != nil { return 0, err } return reply.Atom, nil } func supportsDeleteWindow(conn *xgb.Conn, win xproto.Window) bool { wmProtocols, err := internAtom(conn, "WM_PROTOCOLS") if err != nil { return false } wmDelete, err := internAtom(conn, "WM_DELETE_WINDOW") if err != nil { return false } prop, err := xproto.GetProperty(conn, false, win, wmProtocols, xproto.AtomAtom, 0, 64).Reply() if err != nil || prop == nil || prop.Format != 32 { return false } for i := 0; i+4 <= len(prop.Value); i += 4 { if xproto.Atom(xgb.Get32(prop.Value[i:])) == wmDelete { return true } } return false } func sendDeleteWindow(conn *xgb.Conn, win xproto.Window) error { wmProtocols, err := internAtom(conn, "WM_PROTOCOLS") if err != nil { return err } wmDelete, err := internAtom(conn, "WM_DELETE_WINDOW") if err != nil { return err } data := xproto.ClientMessageDataUnionData32New([]uint32{ uint32(wmDelete), uint32(xproto.TimeCurrentTime), 0, 0, 0, }) ev := xproto.ClientMessageEvent{ Format: 32, Window: win, Type: wmProtocols, Data: data, } return xproto.SendEventChecked(conn, false, win, xproto.EventMaskNoEvent, string(ev.Bytes())).Check() } 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 dim(v int) uint32 { if v < 1 { return 1 } return uint32(v) } 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 := 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 dim(screenWidth - gapSize*2), dim(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(gapSize), // y dim(width), dim(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(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++ } } } 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, int16(int(screen.HeightInPixels)-int(barHeight)), // 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.ConfigWindowY|xproto.ConfigWindowHeight, []uint32{uint32(int(screen.HeightInPixels) - int(barHeight)), 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 BitMask for _, c := range clients { occ |= c.Tags } x := 0 for i := 0; i < tagCount; i++ { var mask BitMask = 1 << uint(i) sel := (GlobalTags & mask) != 0 if !sel && occ&mask == 0 { continue } label := strconv.Itoa(i + 1) fg := barFg if sel { label += "*" fg = selFg } drawBarText(conn, x+int(barHeight)/3, 0, int(barHeight), label, fg, barColor) x += textWidth(conn, label) + int(barHeight)*2/3 } 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) } }