New milestone: can launch and display an xterm
[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
)
type display struct {
screen xinerama.ScreenInfo
windows []xproto.Window
}
func main() {
// Set ourselves up as the new window manager ------------------------------
xconn := connectToXServer()
xroot := getXRoot(xconn)
keymap := getKeyboardMap(xconn)
registerForKeyEvents(xconn, xroot, keymap)
becomeWM(xconn, xroot)
// Build a list of output devices ------------------------------------------
var attachedDisplays []display
for _, screen := range getAttachedScreens(xconn) {
attachedDisplays = append(attachedDisplays, display{screen: screen})
}
// TODO: Verify some displays actually exist and we don't have an empty array. Otherwise, die here.
// TODO: Should this all go in its own function and simply return attachedDisplays[] here instead? What will my fundamental data structure be?
// Build a list of windows needing management ------------------------------
// TODO: The following should all be an initialization function. It should not be here.
tree, err := xproto.QueryTree(xconn, xroot.Root).Reply()
if err != nil {
// TODO: Better error
log.Fatal(err)
}
if tree != nil {
for _, c := range tree.Children {
if isMappedWindow(xconn, c) {
// For now, dump any pre-existing windows into the first
// display. Later we can add functionality for tracking window
// attributes across window manager restarts.
attachedDisplays[0].windows = append(attachedDisplays[0].windows, c)
}
}
// TODO: Try tiling/displaying/whatever with any existing windows before entering the event loop.
}
// Main program loop reacts to X events ------------------------------------
eventloop:
for {
fmt.Printf("ILLI: attachedDisplays: ")
fmt.Println(attachedDisplays)
xevent, err := xconn.WaitForEvent()
if err != nil {
log.Println(err)
continue
}
switch e := xevent.(type) {
case xproto.KeyPressEvent:
fmt.Printf("ILLI: Received xproto.KeyPressEvent\n")
if err := handleKeyPressEvent(keymap, e); err != nil {
break eventloop
}
case xproto.KeyReleaseEvent:
fmt.Printf("ILLI: Received xproto.KeyReleaseEvent\n")
case xproto.DestroyNotifyEvent:
fmt.Printf("ILLI: Received xproto.DestroyNotifyEvent\n")
case xproto.ConfigureRequestEvent:
fmt.Printf("ILLI: Received xproto.ConfigureRequestEvent\n")
rewrittenEvent := xproto.ConfigureNotifyEvent{
Event: e.Window,
Window: e.Window,
AboveSibling: 0,
X: e.X,
Y: e.Y,
Width: e.Width,
Height: e.Height,
BorderWidth: 0,
OverrideRedirect: false,
}
xproto.SendEventChecked(xconn, false, e.Window, xproto.EventMaskStructureNotify, string(rewrittenEvent.Bytes()))
case xproto.MapRequestEvent:
fmt.Printf("ILLI: Received xproto.MapRequestEvent\n")
if windowAttributes, err := xproto.GetWindowAttributes(xconn, e.Window).Reply(); err != nil || !windowAttributes.OverrideRedirect {
xproto.MapWindowChecked(xconn, e.Window)
attachedDisplays[0].windows = append(attachedDisplays[0].windows, e.Window)
if err := xproto.ConfigureWindowChecked (
xconn,
attachedDisplays[0].windows[0],
xproto.ConfigWindowX |
xproto.ConfigWindowY |
xproto.ConfigWindowWidth |
xproto.ConfigWindowHeight,
[]uint32{
uint32(100), // TODO: Temporarily hardcoding for testing.
uint32(100), // TODO: Temporarily hardcoding for testing.
uint32(1720), // TODO: Temporarily hardcoding for testing.
uint32(1000), // TODO: Temporarily hardcoding for testing.
}).Check(); err != nil {
fmt.Println("ILLI: Failed to handle MapRequestEvent")
}
}
case xproto.EnterNotifyEvent:
fmt.Printf("ILLI: Received xproto.EnterNotifyEvent\n")
default:
fmt.Printf("ILLI: Unknown X event received: %s\n", xevent)
}
}
}
// 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(conn *xgb.Conn, windowID xproto.Window) bool {
reply, err := xproto.GetWindowAttributes(conn, 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(keymap [256][]xproto.Keysym, 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?
}