package main import ( "fmt" "log" "os" "os/exec" "github.com/jezek/xgb" "github.com/jezek/xgb/xinerama" "github.com/jezek/xgb/xproto" ) // TODO: Make a separate keysym file and fully populate it. const ( XK_q = 0x0071 XK_Return = 0xff0d ) // TODO: These module-level global variables have arisen while establishing // basic functionality. Before proceeding much further, figure out what to do // with them. var xc *xgb.Conn var attachedScreens []xinerama.ScreenInfo var xroot xproto.ScreenInfo var keymap [256][]xproto.Keysym func main() { // Build a connection to the X server -------------------------------------- xConn, err := xgb.NewConn() if err != nil { log.Fatal(err) } xc = xConn defer xc.Close() // Determine what sort of screen(s) will display output -------------------- setup := xproto.Setup(xc) if setup == nil || len(setup.Roots) < 1 { log.Fatal("ILLI: Unable to parse received SetupInfo from X11 server.") } if err := xinerama.Init(xc); err != nil { log.Fatal(err) } if r, err := xinerama.QueryScreens(xc).Reply(); err != nil { log.Fatal(err) } else { if len(r.ScreenInfo) == 0 { attachedScreens = []xinerama.ScreenInfo{ xinerama.ScreenInfo{ Width: setup.Roots[0].WidthInPixels, Height: setup.Roots[0].HeightInPixels, }, } } else { attachedScreens = r.ScreenInfo } } // Establish a root for the tree ------------------------------------------- connInfo := xproto.Setup(xc) if connInfo == nil { log.Fatal("ILLI: Unable to parse X connection information") } if len(connInfo.Roots) != 1 { log.Fatal("ILLI: Inappropriate number of roots. Did xinerama initialize correctly?") } xroot = connInfo.Roots[0] // Register with the X server as the new window manager -------------------- if err := TakeWMOwnership(); err != nil { if _, ok := err.(xproto.AccessError); ok { log.Fatal("ILLI: Unable to register as window manager with X server. Perhaps another WM is already running?") } log.Fatal(err) } fmt.Println("ILLI: Successfully registered as WM with X server.") // Setup keysym mappings and register for WM-relevant key events ----------- const ( loKey = 8 hiKey = 255 ) m := xproto.GetKeyboardMapping(xc, loKey, hiKey-loKey+1) reply, err := m.Reply() if err != nil { log.Fatal(err) } if reply == nil { log.Fatal("Could not load keyboard map") } for i := 0; i < hiKey-loKey+1; i++ { keymap[loKey+i] = reply.Keysyms[i*int(reply.KeysymsPerKeycode) : (i+1)*int(reply.KeysymsPerKeycode)] } grabs := []struct { sym xproto.Keysym modifiers uint16 codes []xproto.Keycode }{ { sym: XK_q, modifiers: xproto.ModMask1 | xproto.ModMaskShift, }, { sym: XK_Return, modifiers: xproto.ModMask1 | xproto.ModMaskShift, }, } for i, syms := range keymap { for _, sym := range syms { for c := range grabs { if grabs[c].sym == sym { grabs[c].codes = append(grabs[c].codes, xproto.Keycode(i)) } } } } for _, grabbed := range grabs { for _, code := range grabbed.codes { if err := xproto.GrabKeyChecked( xc, false, xroot.Root, grabbed.modifiers, code, xproto.GrabModeAsync, xproto.GrabModeAsync, ).Check(); err != nil { log.Print(err) } } } // Build a list of windows needing management ------------------------------ tree, err := xproto.QueryTree(xc, xroot.Root).Reply() if err != nil { log.Fatal(err) } if tree != nil { //workspaces = make(map[string]*Workspace) //defaultw := &Workspace{mu: &sync.Mutex{}} for _, c := range tree.Children { if isMappedWindow(c) { // err := defaultw.Add(c) // if err != nil { // log.Println(err) // } fmt.Println("ILLI: Found a client.") } } // if len(attachedScreens) > 0 { // defaultw.Screen = &attachedScreens[0] // } // // workspaces["default"] = defaultw // // if err := defaultw.TileWindows(); err != nil { // log.Println(err) // } } // Main program loop reacts to X events ------------------------------------ eventloop: for { xevent, err := xc.WaitForEvent() if err != nil { log.Println(err) continue } switch e := xevent.(type) { case xproto.KeyPressEvent: if err := handleKeyPressEvent(e); err != nil { break eventloop } default: log.Println(xevent) } } } // In addition to the basic key/button press/release events, by registering for // events like SubstructureRedirect, we are declaring our intent to act as // window manager for this X server. Only one WM is allowed per X server, a // policy enforced on the X server's side by refusing the registration request // for events like SubstructureRedirect if an existing process already receives // those events (i.e. is already the window manager). func TakeWMOwnership() error { return xproto.ChangeWindowAttributesChecked( xc, xroot.Root, xproto.CwEventMask, []uint32{ xproto.EventMaskKeyPress | xproto.EventMaskKeyRelease | xproto.EventMaskButtonPress | xproto.EventMaskButtonRelease | xproto.EventMaskStructureNotify | xproto.EventMaskSubstructureRedirect, // TODO: Should we also register for EventMaskSubstructureNotify ? // Where is the authoritative list of events located? }).Check() } // The client list appears to have some entries for windows that shouldn't // actually be viewable. For now, it appears that only windows with a // `MapState` value of 2 should be viewable. TODO: Verify this. // - https://github.com/BurntSushi/xgb/blob/master/xproto/xproto.go#L3772 // - https://github.com/BurntSushi/xgb/blob/master/xproto/xproto.go#L10287 func isMappedWindow(windowID xproto.Window) bool { reply, err := xproto.GetWindowAttributes(xc, windowID).Reply() if err != nil { log.Fatal(err) } if reply != nil && reply.MapState == 2 { return true } return false } // TODO: Determine how I want to split the main event loop from the various // event handlers (like this keypress handler). It shouldn't grow too // fragmented, nor should it grow into a monolithic beast, but the balance // needs to be selected after the handlers are built out more completely. func handleKeyPressEvent(key xproto.KeyPressEvent) error { switch keymap[key.Detail][0] { case XK_q: switch key.State { case xproto.ModMask1 | xproto.ModMaskShift: // TODO: Where and how do I want to handle exit/restart? // ------------------------- // We must manually close the X connection since we used // `defer` when setting it up and os.Exit() short-circuits // that deferral. xc.Close() os.Exit(0) } case XK_Return: switch key.State { case xproto.ModMask1 | xproto.ModMaskShift: cmd := exec.Command("xterm") err := cmd.Start() if err != nil { log.Fatal(err) } } default: return nil } return nil // TODO: What do I actually want to return here? }