Adding some comments after re-reading last night's code commit.
[illi] / illi.go
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?
}