diff options
Diffstat (limited to 'prototypes')
-rw-r--r-- | prototypes/tls-autodetect.go | 451 | ||||
-rw-r--r-- | prototypes/xgb-draw.go | 329 | ||||
-rw-r--r-- | prototypes/xgb-image.go | 313 | ||||
-rw-r--r-- | prototypes/xgb-keys.go | 213 | ||||
-rw-r--r-- | prototypes/xgb-monitors.go | 40 | ||||
-rw-r--r-- | prototypes/xgb-text-viewer.go | 538 | ||||
-rw-r--r-- | prototypes/xgb-window.go | 156 | ||||
-rw-r--r-- | prototypes/xgb-xrender.go | 397 |
8 files changed, 2437 insertions, 0 deletions
diff --git a/prototypes/tls-autodetect.go b/prototypes/tls-autodetect.go new file mode 100644 index 0000000..0427465 --- /dev/null +++ b/prototypes/tls-autodetect.go @@ -0,0 +1,451 @@ +// +// Copyright (c) 2018, Přemysl Janouch <p@janouch.name> +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION +// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +// +// This is an example TLS-autodetecting chat server. +// +// These clients are unable to properly shutdown the connection on their exit: +// telnet localhost 1234 +// openssl s_client -connect localhost:1234 +// +// While this one doesn't react to an EOF from the server: +// ncat -C localhost 1234 +// ncat -C --ssl localhost 1234 +// +package main + +import ( + "bufio" + "crypto/tls" + "flag" + "fmt" + "io" + "log" + "net" + "os" + "os/signal" + "syscall" + "time" +) + +// --- Utilities --------------------------------------------------------------- + +// +// Trivial SSL/TLS autodetection. The first block of data returned by Recvfrom +// must be at least three octets long for this to work reliably, but that should +// not pose a problem in practice. We might try waiting for them. +// +// SSL2: 1xxx xxxx | xxxx xxxx | <1> +// (message length) (client hello) +// SSL3/TLS: <22> | <3> | xxxx xxxx +// (handshake)| (protocol version) +// +func detectTLS(sysconn syscall.RawConn) (isTLS bool) { + sysconn.Read(func(fd uintptr) (done bool) { + var buf [3]byte + n, _, err := syscall.Recvfrom(int(fd), buf[:], syscall.MSG_PEEK) + switch { + case n == 3: + isTLS = buf[0]&0x80 != 0 && buf[2] == 1 + fallthrough + case n == 2: + isTLS = isTLS || buf[0] == 22 && buf[1] == 3 + case n == 1: + isTLS = buf[0] == 22 + case err == syscall.EAGAIN: + return false + } + return true + }) + return isTLS +} + +// --- Declarations ------------------------------------------------------------ + +type connCloseWriter interface { + net.Conn + CloseWrite() error +} + +type client struct { + transport net.Conn // underlying connection + tls *tls.Conn // TLS, if detected + conn connCloseWriter // high-level connection + inQ []byte // unprocessed input + outQ []byte // unprocessed output + reading bool // whether a reading goroutine is running + writing bool // whether a writing goroutine is running + closing bool // whether we're closing the connection + killTimer *time.Timer // timeout +} + +type preparedEvent struct { + client *client + host string // client's hostname or literal IP address + isTLS bool // the client seems to use TLS +} + +type readEvent struct { + client *client + data []byte // new data from the client + err error // read error +} + +type writeEvent struct { + client *client + written int // amount of bytes written + err error // write error +} + +var ( + sigs = make(chan os.Signal, 1) + conns = make(chan net.Conn) + prepared = make(chan preparedEvent) + reads = make(chan readEvent) + writes = make(chan writeEvent) + timeouts = make(chan *client) + + tlsConf *tls.Config + clients = make(map[*client]bool) + listener net.Listener + inShutdown bool + shutdownTimer <-chan time.Time +) + +// --- Server ------------------------------------------------------------------ + +// Broadcast to all /other/ clients (telnet-friendly, also in accordance to +// the plan of extending this to an IRCd). +func broadcast(line string, except *client) { + for c := range clients { + if c != except { + c.send(line) + } + } +} + +// Initiate a clean shutdown of the whole daemon. +func initiateShutdown() { + log.Println("shutting down") + if err := listener.Close(); err != nil { + log.Println(err) + } + for c := range clients { + c.closeLink() + } + + shutdownTimer = time.After(3 * time.Second) + inShutdown = true +} + +// Forcefully tear down all connections. +func forceShutdown(reason string) { + if !inShutdown { + log.Fatalln("forceShutdown called without initiateShutdown") + } + + log.Printf("forced shutdown (%s)\n", reason) + for c := range clients { + c.destroy() + } +} + +// --- Client ------------------------------------------------------------------ + +func (c *client) send(line string) { + if c.conn != nil && !c.closing { + c.outQ = append(c.outQ, (line + "\r\n")...) + c.flushOutQ() + } +} + +// Tear down the client connection, trying to do so in a graceful manner. +func (c *client) closeLink() { + if c.closing { + return + } + if c.conn == nil { + c.destroy() + return + } + + // Since we send this goodbye, we don't need to call CloseWrite here. + c.send("Goodbye") + c.killTimer = time.AfterFunc(3*time.Second, func() { + timeouts <- c + }) + + c.closing = true +} + +// Close the connection and forget about the client. +func (c *client) destroy() { + // Try to send a "close notify" alert if the TLS object is ready, + // otherwise just tear down the transport. + if c.conn != nil { + _ = c.conn.Close() + } else { + _ = c.transport.Close() + } + + // Clean up the goroutine, although a spurious event may still be sent. + if c.killTimer != nil { + c.killTimer.Stop() + } + + log.Println("client destroyed") + delete(clients, c) +} + +// Handle the results from initializing the client's connection. +func (c *client) onPrepared(isTLS bool) { + if isTLS { + c.tls = tls.Server(c.transport, tlsConf) + c.conn = c.tls + } else { + c.conn = c.transport.(connCloseWriter) + } + + // TODO: If we've tried to send any data before now, we need to flushOutQ. + go read(c) + c.reading = true +} + +// Handle the results from trying to read from the client connection. +func (c *client) onRead(data []byte, readErr error) { + if !c.reading { + // Abusing the flag to emulate CloseRead and skip over data, see below. + return + } + + c.inQ = append(c.inQ, data...) + for { + advance, token, _ := bufio.ScanLines(c.inQ, false /* atEOF */) + if advance == 0 { + break + } + + c.inQ = c.inQ[advance:] + line := string(token) + fmt.Println(line) + broadcast(line, c) + } + + if readErr != nil { + c.reading = false + + if readErr != io.EOF { + log.Println(readErr) + c.destroy() + } else if c.closing { + // Disregarding whether a clean shutdown has happened or not. + log.Println("client finished shutdown") + c.destroy() + } else { + log.Println("client EOF") + c.closeLink() + } + } else if len(c.inQ) > 8192 { + log.Println("client inQ overrun") + // TODO: Inform the client about inQ overrun in the farewell message. + c.closeLink() + + // tls.Conn doesn't have the CloseRead method (and it needs to be able + // to read from the TCP connection even for writes, so there isn't much + // sense in expecting the implementation to do anything useful), + // otherwise we'd use it to block incoming packet data. + c.reading = false + } +} + +// Spawn a goroutine to flush the outQ if possible and necessary. +func (c *client) flushOutQ() { + if !c.writing && c.conn != nil { + go write(c, c.outQ) + c.writing = true + } +} + +// Handle the results from trying to write to the client connection. +func (c *client) onWrite(written int, writeErr error) { + c.outQ = c.outQ[written:] + c.writing = false + + if writeErr != nil { + log.Println(writeErr) + c.destroy() + } else if len(c.outQ) > 0 { + c.flushOutQ() + } else if c.closing { + if c.reading { + c.conn.CloseWrite() + } else { + c.destroy() + } + } +} + +// --- Worker goroutines ------------------------------------------------------- + +func accept(ln net.Listener) { + for { + if conn, err := ln.Accept(); err != nil { + // TODO: Consider specific cases in error handling, some errors + // are transitional while others are fatal. + log.Println(err) + break + } else { + conns <- conn + } + } +} + +func prepare(client *client) { + conn := client.transport + host, _, err := net.SplitHostPort(conn.RemoteAddr().String()) + if err != nil { + // In effect, we require TCP/UDP, as they have port numbers. + log.Fatalln(err) + } + + // The Cgo resolver doesn't pthread_cancel getnameinfo threads, so not + // bothering with pointless contexts. + ch := make(chan string, 1) + go func() { + defer close(ch) + if names, err := net.LookupAddr(host); err != nil { + log.Println(err) + } else { + ch <- names[0] + } + }() + + // While we can't cancel it, we still want to set a timeout on it. + select { + case <-time.After(5 * time.Second): + case resolved, ok := <-ch: + if ok { + host = resolved + } + } + + // Note that in this demo application the autodetection prevents non-TLS + // clients from receiving any messages until they send something. + isTLS := false + if sysconn, err := conn.(syscall.Conn).SyscallConn(); err != nil { + // This is just for the TLS detection and doesn't need to be fatal. + log.Println(err) + } else { + isTLS = detectTLS(sysconn) + } + + // FIXME: When the client sends no data, we still initialize its conn. + prepared <- preparedEvent{client, host, isTLS} +} + +func read(client *client) { + // A new buffer is allocated each time we receive some bytes, because of + // thread-safety. Therefore the buffer shouldn't be too large, or we'd + // need to copy it each time into a precisely sized new buffer. + var err error + for err == nil { + var ( + buf [512]byte + n int + ) + n, err = client.conn.Read(buf[:]) + reads <- readEvent{client, buf[:n], err} + } +} + +// Flush outQ, which is passed by parameter so that there are no data races. +func write(client *client, data []byte) { + // We just write as much as we can, the main goroutine does the looping. + n, err := client.conn.Write(data) + writes <- writeEvent{client, n, err} +} + +// --- Main -------------------------------------------------------------------- + +func processOneEvent() { + select { + case <-sigs: + if inShutdown { + forceShutdown("requested by user") + } else { + initiateShutdown() + } + + case <-shutdownTimer: + forceShutdown("timeout") + + case conn := <-conns: + log.Println("accepted client connection") + c := &client{transport: conn} + clients[c] = true + go prepare(c) + + case ev := <-prepared: + log.Println("client is ready, resolved to", ev.host) + if _, ok := clients[ev.client]; ok { + ev.client.onPrepared(ev.isTLS) + } + + case ev := <-reads: + log.Println("received data from client") + if _, ok := clients[ev.client]; ok { + ev.client.onRead(ev.data, ev.err) + } + + case ev := <-writes: + log.Println("sent data to client") + if _, ok := clients[ev.client]; ok { + ev.client.onWrite(ev.written, ev.err) + } + + case c := <-timeouts: + if _, ok := clients[c]; ok { + log.Println("client timeouted") + c.destroy() + } + } +} + +func main() { + // Just deal with unexpected flags, we don't use any ourselves. + flag.Parse() + + if len(flag.Args()) != 3 { + log.Fatalf("usage: %s KEY CERT ADDRESS\n", os.Args[0]) + } + + cert, err := tls.LoadX509KeyPair(flag.Arg(1), flag.Arg(0)) + if err != nil { + log.Fatalln(err) + } + + tlsConf = &tls.Config{Certificates: []tls.Certificate{cert}} + listener, err = net.Listen("tcp", flag.Arg(2)) + if err != nil { + log.Fatalln(err) + } + + go accept(listener) + signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) + + for !inShutdown || len(clients) > 0 { + processOneEvent() + } +} diff --git a/prototypes/xgb-draw.go b/prototypes/xgb-draw.go new file mode 100644 index 0000000..b893f6f --- /dev/null +++ b/prototypes/xgb-draw.go @@ -0,0 +1,329 @@ +// Network-friendly drawing application based on XRender. +// +// TODO: Maybe keep the pixmap as large as the window. +package main + +import ( + "github.com/BurntSushi/xgb" + "github.com/BurntSushi/xgb/render" + "github.com/BurntSushi/xgb/xproto" + "log" +) + +func F64ToFixed(f float64) render.Fixed { return render.Fixed(f * 65536) } +func FixedToF64(f render.Fixed) float64 { return float64(f) / 65536 } + +func findPictureFormat(formats []render.Pictforminfo, + depth byte, direct render.Directformat) render.Pictformat { + for _, pf := range formats { + if pf.Depth == depth && pf.Direct == direct { + return pf.Id + } + } + return 0 +} + +func createNewPicture(X *xgb.Conn, depth byte, drawable xproto.Drawable, + width uint16, height uint16, format render.Pictformat) render.Picture { + pixmapid, err := xproto.NewPixmapId(X) + if err != nil { + log.Fatalln(err) + } + _ = xproto.CreatePixmap(X, depth, pixmapid, drawable, width, height) + + pictid, err := render.NewPictureId(X) + if err != nil { + log.Fatalln(err) + } + _ = render.CreatePicture(X, pictid, xproto.Drawable(pixmapid), format, + 0, []uint32{}) + return pictid +} + +func main() { + X, err := xgb.NewConn() + if err != nil { + log.Fatalln(err) + } + if err := render.Init(X); err != nil { + log.Fatalln(err) + } + + setup := xproto.Setup(X) + screen := setup.DefaultScreen(X) + + visual, depth := screen.RootVisual, screen.RootDepth + if depth < 24 { + log.Fatalln("need more colors") + } + + wid, err := xproto.NewWindowId(X) + if err != nil { + log.Fatalln(err) + } + + _ = xproto.CreateWindow(X, depth, wid, screen.Root, + 0, 0, 500, 500, 0, xproto.WindowClassInputOutput, + visual, xproto.CwBackPixel|xproto.CwEventMask, + []uint32{0xffffffff, xproto.EventMaskButtonPress | + xproto.EventMaskButtonMotion | xproto.EventMaskButtonRelease | + xproto.EventMaskStructureNotify | xproto.EventMaskExposure}) + + title := []byte("Draw") + _ = xproto.ChangeProperty(X, xproto.PropModeReplace, wid, xproto.AtomWmName, + xproto.AtomString, 8, uint32(len(title)), title) + + _ = xproto.MapWindow(X, wid) + + pformats, err := render.QueryPictFormats(X).Reply() + if err != nil { + log.Fatalln(err) + } + + // Find appropriate picture formats. + var pformat, pformatAlpha, pformatRGB render.Pictformat + for _, pd := range pformats.Screens[X.DefaultScreen].Depths { + for _, pv := range pd.Visuals { + if pv.Visual == visual { + pformat = pv.Format + } + } + } + if pformatAlpha = findPictureFormat(pformats.Formats, 8, + render.Directformat{ + AlphaShift: 0, + AlphaMask: 0xff, + }); pformat == 0 { + log.Fatalln("required picture format not found") + } + if pformatRGB = findPictureFormat(pformats.Formats, 24, + render.Directformat{ + RedShift: 16, + RedMask: 0xff, + GreenShift: 8, + GreenMask: 0xff, + BlueShift: 0, + BlueMask: 0xff, + }); pformatRGB == 0 { + log.Fatalln("required picture format not found") + } + + // Picture for the window. + pid, err := render.NewPictureId(X) + if err != nil { + log.Fatalln(err) + } + render.CreatePicture(X, pid, xproto.Drawable(wid), pformat, 0, []uint32{}) + + // Brush shape. + const brushRadius = 5 + + brushid, err := render.NewPictureId(X) + if err != nil { + log.Fatalln(err) + } + + cFull := render.Color{0xffff, 0xffff, 0xffff, 0xffff} + cTrans := render.Color{0xffff, 0xffff, 0xffff, 0} + _ = render.CreateRadialGradient(X, brushid, + render.Pointfix{F64ToFixed(brushRadius), F64ToFixed(brushRadius)}, + render.Pointfix{F64ToFixed(brushRadius), F64ToFixed(brushRadius)}, + F64ToFixed(0), + F64ToFixed(brushRadius), + 3, []render.Fixed{F64ToFixed(0), F64ToFixed(0.1), F64ToFixed(1)}, + []render.Color{cFull, cFull, cTrans}) + + // Brush color. + colorid, err := render.NewPictureId(X) + if err != nil { + log.Fatalln(err) + } + _ = render.CreateSolidFill(X, colorid, render.Color{ + Red: 0x4444, + Green: 0x8888, + Blue: 0xffff, + Alpha: 0xffff, + }) + + // Various pixmaps. + const ( + pixWidth = 1000 + pixHeight = 1000 + ) + + canvasid := createNewPicture(X, 24, + xproto.Drawable(screen.Root), pixWidth, pixHeight, pformatRGB) + bufferid := createNewPicture(X, 24, + xproto.Drawable(screen.Root), pixWidth, pixHeight, pformatRGB) + maskid := createNewPicture(X, 8, + xproto.Drawable(screen.Root), pixWidth, pixHeight, pformatAlpha) + + // Smoothing by way of blur, apparently a misguided idea. + /* + _ = render.SetPictureFilter(X, maskid, + uint16(len("convolution")), "convolution", + []render.Fixed{F64ToFixed(3), F64ToFixed(3), + F64ToFixed(0), F64ToFixed(0.15), F64ToFixed(0), + F64ToFixed(0.15), F64ToFixed(0.40), F64ToFixed(0.15), + F64ToFixed(0), F64ToFixed(0.15), F64ToFixed(0)}) + */ + + // Pixmaps come uninitialized. + _ = render.FillRectangles(X, + render.PictOpSrc, canvasid, render.Color{ + Red: 0xffff, Green: 0xffff, Blue: 0xffff, Alpha: 0xffff, + }, []xproto.Rectangle{{Width: pixWidth, Height: pixHeight}}) + + // This is the only method we can use to render brush strokes without + // alpha accumulation due to stamping. Though this also seems to be + // misguided. Keeping it here for educational purposes. + // + // ConjointOver is defined as: A = Aa * 1 + Ab * max(1-Aa/Ab,0) + // which basically resolves to: A = max(Aa, Ab) + // which equals "lighten" with one channel only. + // + // Resources: + // - https://www.cairographics.org/operators/ + // - http://ssp.impulsetrain.com/porterduff.html + // - https://keithp.com/~keithp/talks/renderproblems/renderproblems/render-title.html + // - https://keithp.com/~keithp/talks/cairo2003.pdf + drawPointAt := func(x, y int16) { + _ = render.Composite(X, render.PictOpConjointOver, + brushid, render.PictureNone, maskid, + 0, 0, 0, 0, x-brushRadius, y-brushRadius, + brushRadius*2, brushRadius*2) + + _ = render.SetPictureClipRectangles(X, bufferid, + x-brushRadius, y-brushRadius, []xproto.Rectangle{ + {Width: brushRadius * 2, Height: brushRadius * 2}}) + _ = render.Composite(X, render.PictOpSrc, + canvasid, render.PictureNone, bufferid, + 0, 0, 0, 0, 0 /* dst-x */, 0, /* dst-y */ + pixWidth, pixHeight) + _ = render.Composite(X, render.PictOpOver, + colorid, maskid, bufferid, + 0, 0, 0, 0, 0 /* dst-x */, 0, /* dst-y */ + pixWidth, pixHeight) + + // Composited, now blit to window without flicker. + _ = render.SetPictureClipRectangles(X, pid, + x-brushRadius, y-brushRadius, []xproto.Rectangle{ + {Width: brushRadius * 2, Height: brushRadius * 2}}) + _ = render.Composite(X, render.PictOpSrc, + bufferid, render.PictureNone, pid, + 0, 0, 0, 0, 0 /* dst-x */, 0, /* dst-y */ + pixWidth, pixHeight) + } + + // Integer version of Bresenham's line drawing algorithm + drawLine := func(x0, y0, x1, y1 int16) { + dx, dy := x1-x0, y1-y0 + if dx < 0 { + dx = -dx + } + if dy < 0 { + dy = -dy + } + + steep := dx < dy + if steep { + // Flip the coordinate system on input + x0, y0 = y0, x0 + x1, y1 = y1, x1 + dx, dy = dy, dx + } + + var stepX, stepY int16 = 1, 1 + if x0 > x1 { + stepX = -1 + } + if y0 > y1 { + stepY = -1 + } + + dpr := dy * 2 + delta := dpr - dx + dpru := delta - dx + + for ; dx > 0; dx-- { + // Unflip the coordinate system on output + if steep { + drawPointAt(y0, x0) + } else { + drawPointAt(x0, y0) + } + + x0 += stepX + if delta > 0 { + y0 += stepY + delta += dpru + } else { + delta += dpr + } + } + } + + var startX, startY int16 = 0, 0 + drawing := false + for { + ev, xerr := X.WaitForEvent() + if xerr != nil { + log.Printf("Error: %s\n", xerr) + return + } + if ev == nil { + return + } + + log.Printf("Event: %s\n", ev) + switch e := ev.(type) { + case xproto.UnmapNotifyEvent: + return + + case xproto.ExposeEvent: + _ = render.SetPictureClipRectangles(X, pid, int16(e.X), int16(e.Y), + []xproto.Rectangle{{Width: e.Width, Height: e.Height}}) + + // Not bothering to deflicker here using the buffer pixmap, + // with compositing this event is rare enough. + _ = render.Composite(X, render.PictOpSrc, + canvasid, render.PictureNone, pid, + 0, 0, 0, 0, 0 /* dst-x */, 0, /* dst-y */ + pixWidth, pixHeight) + + if drawing { + _ = render.Composite(X, render.PictOpOver, + colorid, maskid, pid, + 0, 0, 0, 0, 0 /* dst-x */, 0, /* dst-y */ + pixWidth, pixHeight) + } + + case xproto.ButtonPressEvent: + if e.Detail == xproto.ButtonIndex1 { + render.FillRectangles(X, + render.PictOpSrc, maskid, render.Color{}, + []xproto.Rectangle{{Width: pixWidth, Height: pixHeight}}) + + drawing = true + drawPointAt(e.EventX, e.EventY) + startX, startY = e.EventX, e.EventY + } + + case xproto.MotionNotifyEvent: + if drawing { + drawLine(startX, startY, e.EventX, e.EventY) + startX, startY = e.EventX, e.EventY + } + + case xproto.ButtonReleaseEvent: + if e.Detail == xproto.ButtonIndex1 { + _ = render.Composite(X, render.PictOpOver, + colorid, maskid, canvasid, + 0, 0, 0, 0, 0 /* dst-x */, 0, /* dst-y */ + pixWidth, pixHeight) + + drawing = false + } + } + } +} diff --git a/prototypes/xgb-image.go b/prototypes/xgb-image.go new file mode 100644 index 0000000..2da37d6 --- /dev/null +++ b/prototypes/xgb-image.go @@ -0,0 +1,313 @@ +package main + +import ( + "encoding/binary" + "github.com/BurntSushi/xgb" + "github.com/BurntSushi/xgb/render" + "github.com/BurntSushi/xgb/shm" + "github.com/BurntSushi/xgb/xproto" + "log" + "os" + "reflect" + "time" + "unsafe" + + "image" + _ "image/gif" + _ "image/jpeg" + _ "image/png" +) + +// #include <sys/ipc.h> +// #include <sys/shm.h> +import "C" + +func F64ToFixed(f float64) render.Fixed { return render.Fixed(f * 65536) } +func FixedToF64(f render.Fixed) float64 { return float64(f) / 65536 } + +func main() { + /* + pf, err := os.Create("pprof.out") + if err != nil { + log.Fatal(err) + } + pprof.StartCPUProfile(pf) + defer pprof.StopCPUProfile() + */ + + // Load a picture from the command line. + f, err := os.Open(os.Args[1]) + if err != nil { + log.Fatalln(err) + } + defer f.Close() + + img, name, err := image.Decode(f) + if err != nil { + log.Fatalln(err) + } + log.Println("image type is", name) + + // Miscellaneous X11 initialization. + X, err := xgb.NewConn() + if err != nil { + log.Fatalln(err) + } + + if err := render.Init(X); err != nil { + log.Fatalln(err) + } + + setup := xproto.Setup(X) + screen := setup.DefaultScreen(X) + + visual, depth := screen.RootVisual, screen.RootDepth + // TODO: We should check that we find it, though we don't /need/ alpha here, + // it's just a minor improvement--affects the backpixel value. + for _, i := range screen.AllowedDepths { + for _, v := range i.Visuals { + // TODO: Could/should check other parameters. + if i.Depth == 32 && v.Class == xproto.VisualClassTrueColor { + visual, depth = v.VisualId, i.Depth + break + } + } + } + + mid, err := xproto.NewColormapId(X) + if err != nil { + log.Fatalln(err) + } + + _ = xproto.CreateColormap( + X, xproto.ColormapAllocNone, mid, screen.Root, visual) + + wid, err := xproto.NewWindowId(X) + if err != nil { + log.Fatalln(err) + } + + // Border pixel and colormap are required when depth differs from parent. + _ = xproto.CreateWindow(X, depth, wid, screen.Root, + 0, 0, 500, 500, 0, xproto.WindowClassInputOutput, + visual, xproto.CwBackPixel|xproto.CwBorderPixel|xproto.CwEventMask| + xproto.CwColormap, []uint32{0x80808080, 0, + xproto.EventMaskStructureNotify | xproto.EventMaskExposure, + uint32(mid)}) + + title := []byte("Image") + _ = xproto.ChangeProperty(X, xproto.PropModeReplace, wid, xproto.AtomWmName, + xproto.AtomString, 8, uint32(len(title)), title) + + _ = xproto.MapWindow(X, wid) + + pformats, err := render.QueryPictFormats(X).Reply() + if err != nil { + log.Fatalln(err) + } + + // Similar to XRenderFindVisualFormat. + // The DefaultScreen is almost certain to be zero. + var pformat render.Pictformat + for _, pd := range pformats.Screens[X.DefaultScreen].Depths { + // This check seems to be slightly extraneous. + if pd.Depth != depth { + continue + } + for _, pv := range pd.Visuals { + if pv.Visual == visual { + pformat = pv.Format + } + } + } + + // Wrap the window's surface in a picture. + pid, err := render.NewPictureId(X) + if err != nil { + log.Fatalln(err) + } + render.CreatePicture(X, pid, xproto.Drawable(wid), pformat, 0, []uint32{}) + + // setup.BitmapFormatScanline{Pad,Unit} and setup.BitmapFormatBitOrder + // don't interest us here since we're only using Z format pixmaps. + for _, pf := range setup.PixmapFormats { + if pf.Depth == 32 { + if pf.BitsPerPixel != 32 || pf.ScanlinePad != 32 { + log.Fatalln("unsuported X server") + } + } + } + + pixid, err := xproto.NewPixmapId(X) + if err != nil { + log.Fatalln(err) + } + _ = xproto.CreatePixmap(X, 32, pixid, xproto.Drawable(screen.Root), + uint16(img.Bounds().Dx()), uint16(img.Bounds().Dy())) + + var bgraFormat render.Pictformat + wanted := render.Directformat{ + RedShift: 16, + RedMask: 0xff, + GreenShift: 8, + GreenMask: 0xff, + BlueShift: 0, + BlueMask: 0xff, + AlphaShift: 24, + AlphaMask: 0xff, + } + for _, pf := range pformats.Formats { + if pf.Depth == 32 && pf.Direct == wanted { + bgraFormat = pf.Id + break + } + } + if bgraFormat == 0 { + log.Fatalln("ARGB format not found") + } + + // We could also look for the inverse pictformat. + var encoding binary.ByteOrder + if setup.ImageByteOrder == xproto.ImageOrderMSBFirst { + encoding = binary.BigEndian + } else { + encoding = binary.LittleEndian + } + + pixpicid, err := render.NewPictureId(X) + if err != nil { + log.Fatalln(err) + } + render.CreatePicture(X, pixpicid, xproto.Drawable(pixid), bgraFormat, + 0, []uint32{}) + + // Do we really need this? :/ + cid, err := xproto.NewGcontextId(X) + if err != nil { + log.Fatalln(err) + } + _ = xproto.CreateGC(X, cid, xproto.Drawable(pixid), + xproto.GcGraphicsExposures, []uint32{0}) + + bounds := img.Bounds() + Lstart := time.Now() + + if err := shm.Init(X); err != nil { + log.Println("MIT-SHM unavailable") + + // We're being lazy and resolve the 1<<16 limit of requests by sending + // a row at a time. The encoding is also done inefficiently. + // Also see xgbutil/xgraphics/xsurface.go. + row := make([]byte, bounds.Dx()*4) + for y := bounds.Min.Y; y < bounds.Max.Y; y++ { + for x := bounds.Min.X; x < bounds.Max.X; x++ { + r, g, b, a := img.At(x, y).RGBA() + encoding.PutUint32(row[x*4:], + (a>>8)<<24|(r>>8)<<16|(g>>8)<<8|(b>>8)) + } + _ = xproto.PutImage(X, xproto.ImageFormatZPixmap, + xproto.Drawable(pixid), cid, uint16(bounds.Dx()), 1, + 0, int16(y), + 0, 32, row) + } + } else { + rep, err := shm.QueryVersion(X).Reply() + if err != nil { + log.Fatalln(err) + } + if rep.PixmapFormat != xproto.ImageFormatZPixmap || + !rep.SharedPixmaps { + log.Fatalln("MIT-SHM configuration unfit") + } + + shmSize := bounds.Dx() * bounds.Dy() * 4 + + // As a side note, to clean up unreferenced segments (orphans): + // ipcs -m | awk '$6 == "0" { print $2 }' | xargs ipcrm shm + shmID := int(C.shmget(C.IPC_PRIVATE, + C.size_t(shmSize), C.IPC_CREAT|0777)) + if shmID == -1 { + // TODO: We should handle this case by falling back to PutImage, + // if only because the allocation may hit a system limit. + log.Fatalln("memory allocation failed") + } + + dataRaw := C.shmat(C.int(shmID), nil, 0) + defer C.shmdt(dataRaw) + defer C.shmctl(C.int(shmID), C.IPC_RMID, nil) + + data := *(*[]byte)(unsafe.Pointer(&reflect.SliceHeader{ + Data: uintptr(dataRaw), Len: shmSize, Cap: shmSize})) + for y := bounds.Min.Y; y < bounds.Max.Y; y++ { + row := data[y*bounds.Dx()*4:] + for x := bounds.Min.X; x < bounds.Max.X; x++ { + r, g, b, a := img.At(x, y).RGBA() + encoding.PutUint32(row[x*4:], + (a>>8)<<24|(r>>8)<<16|(g>>8)<<8|(b>>8)) + } + } + + segid, err := shm.NewSegId(X) + if err != nil { + log.Fatalln(err) + } + + // Need to have it attached on the server before we unload the segment. + c := shm.AttachChecked(X, segid, uint32(shmID), true /* RO */) + if err := c.Check(); err != nil { + log.Fatalln(err) + } + + _ = shm.PutImage(X, xproto.Drawable(pixid), cid, + uint16(bounds.Dx()), uint16(bounds.Dy()), 0, 0, + uint16(bounds.Dx()), uint16(bounds.Dy()), 0, 0, + 32, xproto.ImageFormatZPixmap, + 0 /* SendEvent */, segid, 0 /* Offset */) + } + + log.Println("uploading took", time.Now().Sub(Lstart)) + + var scale float64 = 1 + for { + ev, xerr := X.WaitForEvent() + if xerr != nil { + log.Printf("Error: %s\n", xerr) + return + } + if ev == nil { + return + } + + log.Printf("Event: %s\n", ev) + switch e := ev.(type) { + case xproto.UnmapNotifyEvent: + return + + case xproto.ConfigureNotifyEvent: + w, h := e.Width, e.Height + + scaleX := float64(bounds.Dx()) / float64(w) + scaleY := float64(bounds.Dy()) / float64(h) + + if scaleX < scaleY { + scale = scaleY + } else { + scale = scaleX + } + + _ = render.SetPictureTransform(X, pixpicid, render.Transform{ + F64ToFixed(scale), F64ToFixed(0), F64ToFixed(0), + F64ToFixed(0), F64ToFixed(scale), F64ToFixed(0), + F64ToFixed(0), F64ToFixed(0), F64ToFixed(1), + }) + _ = render.SetPictureFilter(X, pixpicid, 8, "bilinear", nil) + + case xproto.ExposeEvent: + _ = render.Composite(X, render.PictOpSrc, + pixpicid, render.PictureNone, pid, + 0, 0, 0, 0, 0 /* dst-x */, 0, /* dst-y */ + uint16(float64(img.Bounds().Dx())/scale), + uint16(float64(img.Bounds().Dy())/scale)) + } + } +} diff --git a/prototypes/xgb-keys.go b/prototypes/xgb-keys.go new file mode 100644 index 0000000..578ab39 --- /dev/null +++ b/prototypes/xgb-keys.go @@ -0,0 +1,213 @@ +package main + +import ( + "github.com/BurntSushi/xgb" + //"github.com/BurntSushi/xgb/xkb" + "github.com/BurntSushi/xgb/xproto" + "log" +) + +func main() { + X, err := xgb.NewConn() + if err != nil { + log.Fatalln(err) + } + + /* + // Use the extension if available, makes better use of state bits. + if err := xkb.Init(X); err == nil { + if _, err := xkb.UseExtension(X, 1, 0).Reply(); err != nil { + log.Fatalln(err) + } + } + */ + + setup := xproto.Setup(X) + screen := setup.DefaultScreen(X) + + visual, depth := screen.RootVisual, screen.RootDepth + // TODO: We should check that we find it, though we don't /need/ alpha here, + // it's just a minor improvement--affects the backpixel value. + for _, i := range screen.AllowedDepths { + for _, v := range i.Visuals { + // TODO: Could/should check other parameters. + if i.Depth == 32 && v.Class == xproto.VisualClassTrueColor { + visual, depth = v.VisualId, i.Depth + break + } + } + } + + mid, err := xproto.NewColormapId(X) + if err != nil { + log.Fatalln(err) + } + + _ = xproto.CreateColormap( + X, xproto.ColormapAllocNone, mid, screen.Root, visual) + + wid, err := xproto.NewWindowId(X) + if err != nil { + log.Fatalln(err) + } + + // Border pixel and colormap are required when depth differs from parent. + _ = xproto.CreateWindow(X, depth, wid, screen.Root, + 0, 0, 500, 500, 0, xproto.WindowClassInputOutput, + visual, xproto.CwBackPixel|xproto.CwBorderPixel|xproto.CwEventMask| + xproto.CwColormap, []uint32{0x80808080, 0, + xproto.EventMaskStructureNotify | xproto.EventMaskKeyPress | + /* KeymapNotify */ xproto.EventMaskKeymapState, uint32(mid)}) + + title := []byte("Keys") + _ = xproto.ChangeProperty(X, xproto.PropModeReplace, wid, xproto.AtomWmName, + xproto.AtomString, 8, uint32(len(title)), title) + + _ = xproto.MapWindow(X, wid) + + mapping, err := xproto.GetKeyboardMapping(X, setup.MinKeycode, + byte(setup.MaxKeycode-setup.MinKeycode+1)).Reply() + if err != nil { + log.Fatalln(err) + } + + // The order is "Shift, Lock, Control, Mod1, Mod2, Mod3, Mod4, and Mod5." + mm, err := xproto.GetModifierMapping(X).Reply() + if err != nil { + log.Fatalln(err) + } + + // XXX: This seems pointless, the key will just end up switching groups + // instead of levels without full XKB handling. Though perhaps it might + // at least work as intended when there's only one XKB group. + const MODE_SWITCH = 0xff7e + + var modeSwitchMask uint16 + for mod := 0; mod < 8; mod++ { + perMod := int(mm.KeycodesPerModifier) + for _, kc := range mm.Keycodes[mod*perMod : (mod+1)*perMod] { + if kc == 0 { + continue + } + + perKc := int(mapping.KeysymsPerKeycode) + k := int(kc - setup.MinKeycode) + for _, ks := range mapping.Keysyms[k*perKc : (k+1)*perKc] { + if ks == MODE_SWITCH { + modeSwitchMask |= 1 << uint(mod) + } + } + } + } + + for { + ev, xerr := X.WaitForEvent() + if xerr != nil { + log.Printf("Error: %s\n", xerr) + return + } + if ev == nil { + return + } + + log.Printf("Event: %s\n", ev) + switch e := ev.(type) { + case xproto.UnmapNotifyEvent: + return + + case xproto.KeymapNotifyEvent: + // e.Keys is a 32 * 8 large bitmap indicating which keys are + // currently pressed down. This is sent "after every EnterNotify + // and FocusIn" but it also seems to fire when the keyboard layout + // changes. aixterm manual even speaks of that explicitly. + // + // But since changing the effective group involves no changes to + // the compatibility mapping, there's nothing for us to do. + + case xproto.MappingNotifyEvent: + // e.FirstKeyCode .. e.Count changes have happened but rereading + // everything is the simpler thing to do. + mapping, err = xproto.GetKeyboardMapping(X, setup.MinKeycode, + byte(setup.MaxKeycode-setup.MinKeycode+1)).Reply() + if err != nil { + log.Fatalln(err) + } + + // TODO: We should also repeat the search for MODE SWITCH. + + case xproto.KeyPressEvent: + step := int(mapping.KeysymsPerKeycode) + from := int(e.Detail-setup.MinKeycode) * step + ks := mapping.Keysyms[from : from+step] + + // Strip trailing NoSymbol entries. + for len(ks) > 0 && ks[len(ks)-1] == 0 { + ks = ks[:len(ks)-1] + } + + // Expand back to at least 4. + switch { + case len(ks) == 1: + ks = append(ks, 0, ks[0], 0) + case len(ks) == 2: + ks = append(ks, ks[0], ks[1]) + case len(ks) == 3: + ks = append(ks, 0) + } + + // Other silly expansion rules, only applied to basic ASCII. + if ks[1] == 0 { + ks[1] = ks[0] + if ks[0] >= 'A' && ks[0] <= 'Z' || + ks[0] >= 'a' && ks[0] <= 'z' { + ks[0] = ks[0] | 32 + ks[1] = ks[0] &^ 32 + } + } + + if ks[3] == 0 { + ks[3] = ks[2] + if ks[2] >= 'A' && ks[2] <= 'Z' || + ks[2] >= 'a' && ks[2] <= 'z' { + ks[2] = ks[2] | 32 + ks[3] = ks[2] &^ 32 + } + } + + // We only have enough information to switch between two groups. + offset := 0 + if e.State&modeSwitchMask != 0 { + offset += 2 + } + + var result xproto.Keysym + + shift := e.State&xproto.ModMaskShift != 0 + lock := e.State&xproto.ModMaskLock != 0 + switch { + case !shift && !lock: + result = ks[offset+0] + case !shift && lock: + if ks[offset+0] >= 'a' && ks[offset+0] <= 'z' { + result = ks[offset+1] + } else { + result = ks[offset+0] + } + case shift && lock: + if ks[offset+1] >= 'a' && ks[offset+1] <= 'z' { + result = ks[offset+1] &^ 32 + } else { + result = ks[offset+1] + } + case shift: + result = ks[offset+1] + } + + if result <= 0xff { + log.Printf("%c (Latin-1)\n", rune(result)) + } else { + log.Println(result) + } + } + } +} diff --git a/prototypes/xgb-monitors.go b/prototypes/xgb-monitors.go new file mode 100644 index 0000000..5ce5af9 --- /dev/null +++ b/prototypes/xgb-monitors.go @@ -0,0 +1,40 @@ +package main + +import ( + "github.com/BurntSushi/xgb" + "github.com/BurntSushi/xgb/xproto" + "log" + + // Needs a patched local version with xcb-proto 1.12 and this fix: + // -size := xgb.Pad((8 + (24 + xgb.Pad((int(NOutput) * 4))))) + // +size := xgb.Pad((8 + (24 + xgb.Pad((int(Monitorinfo.NOutput) * 4))))) + "github.com/BurntSushi/xgb/randr" +) + +func main() { + X, err := xgb.NewConn() + if err != nil { + log.Fatalln(err) + } + + if err := randr.Init(X); err != nil { + log.Fatalln(err) + } + + setup := xproto.Setup(X) + screen := setup.DefaultScreen(X) + + ms, err := randr.GetMonitors(X, screen.Root, true /* GetActive */).Reply() + if err != nil { + log.Fatalln(err) + } + + for _, m := range ms.Monitors { + reply, err := xproto.GetAtomName(X, m.Name).Reply() + if err != nil { + log.Fatalln(err) + } + + log.Printf("Monitor %s %+v\n", reply.Name, m) + } +} diff --git a/prototypes/xgb-text-viewer.go b/prototypes/xgb-text-viewer.go new file mode 100644 index 0000000..e740138 --- /dev/null +++ b/prototypes/xgb-text-viewer.go @@ -0,0 +1,538 @@ +// This is an amalgamation of xgb-xrender.go and xgb-keys.go and more of a demo, +// some comments have been stripped. +package main + +import ( + "github.com/BurntSushi/xgb" + "github.com/BurntSushi/xgb/render" + "github.com/BurntSushi/xgb/xproto" + "github.com/golang/freetype" + "github.com/golang/freetype/truetype" + "golang.org/x/image/font" + "golang.org/x/image/font/gofont/goregular" + "golang.org/x/image/math/fixed" + "image" + "image/draw" + "io/ioutil" + "log" + "os" + "strings" +) + +func glyphListBytes(buf []byte, runes []rune, size int) int { + b := 0 + for _, r := range runes { + switch size { + default: + buf[b] = byte(r) + b += 1 + case 2: + xgb.Put16(buf[b:], uint16(r)) + b += 2 + case 4: + xgb.Put32(buf[b:], uint32(r)) + b += 4 + } + } + return xgb.Pad(b) +} + +// When the len is 255, a GLYPHABLE follows, otherwise a list of CARD8/16/32. +func glyphEltHeaderBytes(buf []byte, len byte, deltaX, deltaY int16) int { + b := 0 + buf[b] = len + b += 4 + xgb.Put16(buf[b:], uint16(deltaX)) + b += 2 + xgb.Put16(buf[b:], uint16(deltaY)) + b += 2 + return xgb.Pad(b) +} + +type xgbCookie interface{ Check() error } + +// compositeString makes an appropriate render.CompositeGlyphs request, +// assuming that glyphs equal Unicode codepoints. +func compositeString(c *xgb.Conn, op byte, src, dst render.Picture, + maskFormat render.Pictformat, glyphset render.Glyphset, srcX, srcY int16, + destX, destY int16, text string) xgbCookie { + runes := []rune(text) + + var highest rune + for _, r := range runes { + if r > highest { + highest = r + } + } + + size := 1 + switch { + case highest > 1<<16: + size = 4 + case highest > 1<<8: + size = 2 + } + + // They gave up on the XCB protocol API and we need to serialize explicitly. + + // To spare us from caring about the padding, use the largest number lesser + // than 255 that is divisible by 4 (for size 2 and 4 the requirements are + // less strict but this works in the general case). + const maxPerChunk = 252 + + buf := make([]byte, (len(runes)+maxPerChunk-1)/maxPerChunk*8+len(runes)*size) + b := 0 + + for len(runes) > maxPerChunk { + b += glyphEltHeaderBytes(buf[b:], maxPerChunk, 0, 0) + b += glyphListBytes(buf[b:], runes[:maxPerChunk], size) + runes = runes[maxPerChunk:] + } + if len(runes) > 0 { + b += glyphEltHeaderBytes(buf[b:], byte(len(runes)), destX, destY) + b += glyphListBytes(buf[b:], runes, size) + } + + switch size { + default: + return render.CompositeGlyphs8(c, op, src, dst, maskFormat, glyphset, + srcX, srcY, buf) + case 2: + return render.CompositeGlyphs16(c, op, src, dst, maskFormat, glyphset, + srcX, srcY, buf) + case 4: + return render.CompositeGlyphs32(c, op, src, dst, maskFormat, glyphset, + srcX, srcY, buf) + } +} + +type textRenderer struct { + f *truetype.Font + opts *truetype.Options + face font.Face + + bounds fixed.Rectangle26_6 // outer bounds for all the font's glyph + buf *image.RGBA // rendering buffer + + X *xgb.Conn + gsid render.Glyphset + loaded map[rune]bool +} + +func newTextRenderer(X *xgb.Conn, ttf []byte, opts *truetype.Options) ( + *textRenderer, error) { + pformats, err := render.QueryPictFormats(X).Reply() + if err != nil { + return nil, err + } + + // We use RGBA here just so that lines are padded to 32 bits. + // Since there's no subpixel antialiasing and alpha is premultiplied, + // it doesn't even mater that RGBA is interpreted as ARGB or BGRA. + var rgbFormat render.Pictformat + for _, pf := range pformats.Formats { + if pf.Depth == 32 && pf.Direct.AlphaMask != 0 { + rgbFormat = pf.Id + break + } + } + + tr := &textRenderer{opts: opts, X: X, loaded: make(map[rune]bool)} + if tr.f, err = freetype.ParseFont(goregular.TTF); err != nil { + return nil, err + } + + tr.face = truetype.NewFace(tr.f, opts) + tr.bounds = tr.f.Bounds(fixed.Int26_6(opts.Size * float64(opts.DPI) * + (64.0 / 72.0))) + + if tr.gsid, err = render.NewGlyphsetId(X); err != nil { + return nil, err + } + if err := render.CreateGlyphSetChecked(X, tr.gsid, rgbFormat). + Check(); err != nil { + return nil, err + } + + tr.buf = image.NewRGBA(image.Rect( + +tr.bounds.Min.X.Floor(), + -tr.bounds.Min.Y.Floor(), + +tr.bounds.Max.X.Ceil(), + -tr.bounds.Max.Y.Ceil(), + )) + return tr, nil +} + +func (tr *textRenderer) addRune(r rune) bool { + dr, mask, maskp, advance, ok := tr.face.Glyph( + fixed.P(0, 0) /* subpixel destination location */, r) + if !ok { + return false + } + + for i := 0; i < len(tr.buf.Pix); i++ { + tr.buf.Pix[i] = 0 + } + + // Copying, since there are absolutely no guarantees. + draw.Draw(tr.buf, dr, mask, maskp, draw.Src) + + _ = render.AddGlyphs(tr.X, tr.gsid, 1, []uint32{uint32(r)}, + []render.Glyphinfo{{ + Width: uint16(tr.buf.Rect.Size().X), + Height: uint16(tr.buf.Rect.Size().Y), + X: int16(-tr.bounds.Min.X.Floor()), + Y: int16(+tr.bounds.Max.Y.Ceil()), + XOff: int16(advance.Ceil()), + YOff: int16(0), + }}, []byte(tr.buf.Pix)) + return true +} + +func (tr *textRenderer) render(src, dst render.Picture, + srcX, srcY, destX, destY int16, text string) xgbCookie { + // XXX: You're really supposed to handle tabs differently from this. + text = strings.Replace(text, "\t", " ", -1) + + for _, r := range text { + if !tr.loaded[r] { + tr.addRune(r) + tr.loaded[r] = true + } + } + + return compositeString(tr.X, render.PictOpOver, src, dst, + 0 /* TODO: mask Pictureformat? */, tr.gsid, + srcX, srcY, destX, destY, text) +} + +const ( + ksEscape = 0xff1b + ksUp = 0xff52 + ksDown = 0xff54 + ksPageUp = 0xff55 + ksPageDown = 0xff56 + ksModeSwitch = 0xff7e +) + +type keyMapper struct { + X *xgb.Conn + setup *xproto.SetupInfo + mapping *xproto.GetKeyboardMappingReply + + modeSwitchMask uint16 +} + +func newKeyMapper(X *xgb.Conn) (*keyMapper, error) { + m := &keyMapper{X: X, setup: xproto.Setup(X)} + if err := m.update(); err != nil { + return nil, err + } + return m, nil +} + +func (km *keyMapper) update() error { + var err error + km.mapping, err = xproto.GetKeyboardMapping(km.X, km.setup.MinKeycode, + byte(km.setup.MaxKeycode-km.setup.MinKeycode+1)).Reply() + if err != nil { + return err + } + + km.modeSwitchMask = 0 + + // The order is "Shift, Lock, Control, Mod1, Mod2, Mod3, Mod4, and Mod5." + mm, err := xproto.GetModifierMapping(km.X).Reply() + if err != nil { + return err + } + + perMod := int(mm.KeycodesPerModifier) + perKc := int(km.mapping.KeysymsPerKeycode) + for mod := 0; mod < 8; mod++ { + for _, kc := range mm.Keycodes[mod*perMod : (mod+1)*perMod] { + if kc == 0 { + continue + } + + k := int(kc - km.setup.MinKeycode) + for _, ks := range km.mapping.Keysyms[k*perKc : (k+1)*perKc] { + if ks == ksModeSwitch { + km.modeSwitchMask |= 1 << uint(mod) + } + } + } + } + return nil +} + +func (km *keyMapper) decode(e xproto.KeyPressEvent) (result xproto.Keysym) { + step := int(km.mapping.KeysymsPerKeycode) + from := int(e.Detail-km.setup.MinKeycode) * step + ks := km.mapping.Keysyms[from : from+step] + + // Strip trailing NoSymbol entries. + for len(ks) > 0 && ks[len(ks)-1] == 0 { + ks = ks[:len(ks)-1] + } + + // Expand back to at least 4. + switch { + case len(ks) == 1: + ks = append(ks, 0, ks[0], 0) + case len(ks) == 2: + ks = append(ks, ks[0], ks[1]) + case len(ks) == 3: + ks = append(ks, 0) + } + + // Other silly expansion rules, only applied to basic ASCII since we + // don't have translation tables to Unicode here for brevity. + if ks[1] == 0 { + ks[1] = ks[0] + if ks[0] >= 'A' && ks[0] <= 'Z' || + ks[0] >= 'a' && ks[0] <= 'z' { + ks[0] = ks[0] | 32 + ks[1] = ks[0] &^ 32 + } + } + + if ks[3] == 0 { + ks[3] = ks[2] + if ks[2] >= 'A' && ks[2] <= 'Z' || + ks[2] >= 'a' && ks[2] <= 'z' { + ks[2] = ks[2] | 32 + ks[3] = ks[2] &^ 32 + } + } + + offset := 0 + if e.State&km.modeSwitchMask != 0 { + offset += 2 + } + + shift := e.State&xproto.ModMaskShift != 0 + lock := e.State&xproto.ModMaskLock != 0 + switch { + case !shift && !lock: + result = ks[offset+0] + case !shift && lock: + if ks[offset+0] >= 'a' && ks[offset+0] <= 'z' { + result = ks[offset+1] + } else { + result = ks[offset+0] + } + case shift && lock: + if ks[offset+1] >= 'a' && ks[offset+1] <= 'z' { + result = ks[offset+1] &^ 32 + } else { + result = ks[offset+1] + } + case shift: + result = ks[offset+1] + } + return +} + +func main() { + if len(os.Args) < 2 { + log.Fatalln("no filename given") + } + + text, err := ioutil.ReadFile(os.Args[1]) + if err != nil { + log.Fatalln(err) + } + lines := strings.Split(string(text), "\n") + + X, err := xgb.NewConn() + if err != nil { + log.Fatalln(err) + } + if err := render.Init(X); err != nil { + log.Fatalln(err) + } + + setup := xproto.Setup(X) + screen := setup.DefaultScreen(X) + + visual, depth := screen.RootVisual, screen.RootDepth + // TODO: We should check that we find it, though we don't /need/ alpha here, + // it's just a minor improvement--affects the backpixel value. + for _, i := range screen.AllowedDepths { + // TODO: Could/should check other parameters. + for _, v := range i.Visuals { + if i.Depth == 32 && v.Class == xproto.VisualClassTrueColor { + visual, depth = v.VisualId, i.Depth + break + } + } + } + + mid, err := xproto.NewColormapId(X) + if err != nil { + log.Fatalln(err) + } + _ = xproto.CreateColormap( + X, xproto.ColormapAllocNone, mid, screen.Root, visual) + + wid, err := xproto.NewWindowId(X) + if err != nil { + log.Fatalln(err) + } + // Border pixel and colormap are required when depth differs from parent. + _ = xproto.CreateWindow(X, depth, wid, screen.Root, + 0, 0, 500, 500, 0, xproto.WindowClassInputOutput, + visual, xproto.CwBackPixel|xproto.CwBorderPixel|xproto.CwEventMask| + xproto.CwColormap, []uint32{0xf0f0f0f0, 0, + xproto.EventMaskStructureNotify | xproto.EventMaskKeyPress | + /* KeymapNotify */ xproto.EventMaskKeymapState | + xproto.EventMaskExposure | xproto.EventMaskButtonPress, + uint32(mid)}) + + title := []byte("Viewer") + _ = xproto.ChangeProperty(X, xproto.PropModeReplace, wid, xproto.AtomWmName, + xproto.AtomString, 8, uint32(len(title)), title) + + _ = xproto.MapWindow(X, wid) + + pformats, err := render.QueryPictFormats(X).Reply() + if err != nil { + log.Fatalln(err) + } + + // Similar to XRenderFindVisualFormat. + // The DefaultScreen is almost certain to be zero. + var pformat render.Pictformat + for _, pd := range pformats.Screens[X.DefaultScreen].Depths { + // This check seems to be slightly extraneous. + if pd.Depth != depth { + continue + } + for _, pv := range pd.Visuals { + if pv.Visual == visual { + pformat = pv.Format + } + } + } + + pid, err := render.NewPictureId(X) + if err != nil { + log.Fatalln(err) + } + render.CreatePicture(X, pid, xproto.Drawable(wid), pformat, 0, []uint32{}) + + blackid, err := render.NewPictureId(X) + if err != nil { + log.Fatalln(err) + } + _ = render.CreateSolidFill(X, blackid, render.Color{Alpha: 0xffff}) + + tr, err := newTextRenderer(X, goregular.TTF, &truetype.Options{ + Size: 10, + DPI: float64(screen.WidthInPixels) / + float64(screen.WidthInMillimeters) * 25.4, + Hinting: font.HintingFull, + }) + if err != nil { + log.Fatalln(err) + } + + scroll := 0 // index of the top line + + var w, h uint16 + redraw := func() { + y, ascent, step := 5, tr.bounds.Max.Y.Ceil(), + tr.bounds.Max.Y.Ceil()-tr.bounds.Min.Y.Floor() + for _, line := range lines[scroll:] { + if uint16(y) >= h { + break + } + _ = tr.render(blackid, pid, 0, 0, 5, int16(y+ascent), line) + y += step + } + + vis := float64(h-10) / float64(step) + if vis < float64(len(lines)) { + length := float64(step) * (vis + 1) * vis / float64(len(lines)) + start := float64(step) * float64(scroll) * vis / float64(len(lines)) + + _ = render.FillRectangles(X, render.PictOpSrc, pid, + render.Color{Alpha: 0xffff}, []xproto.Rectangle{{ + X: int16(w - 15), Y: int16(start), + Width: 15, Height: uint16(length + 10)}}) + } + } + + km, err := newKeyMapper(X) + if err != nil { + log.Fatalln(err) + } + + for { + ev, xerr := X.WaitForEvent() + if xerr != nil { + log.Printf("Error: %s\n", xerr) + return + } + if ev == nil { + return + } + + switch e := ev.(type) { + case xproto.UnmapNotifyEvent: + return + + case xproto.ConfigureNotifyEvent: + w, h = e.Width, e.Height + + case xproto.MappingNotifyEvent: + _ = km.update() + + case xproto.KeyPressEvent: + _ = xproto.ClearArea(X, true /* ExposeEvent */, wid, 0, 0, w, h) + + const pageJump = 40 + switch km.decode(e) { + case ksEscape: + return + case ksUp: + if scroll >= 1 { + scroll-- + } + case ksDown: + if scroll+1 < len(lines) { + scroll++ + } + case ksPageUp: + if scroll >= pageJump { + scroll -= pageJump + } + case ksPageDown: + if scroll+pageJump < len(lines) { + scroll += pageJump + } + } + + case xproto.ButtonPressEvent: + _ = xproto.ClearArea(X, true /* ExposeEvent */, wid, 0, 0, w, h) + + switch e.Detail { + case xproto.ButtonIndex4: + if scroll > 0 { + scroll-- + } + case xproto.ButtonIndex5: + if scroll+1 < len(lines) { + scroll++ + } + } + + case xproto.ExposeEvent: + // FIXME: The window's context haven't necessarily been destroyed. + if e.Count == 0 { + redraw() + } + } + } +} diff --git a/prototypes/xgb-window.go b/prototypes/xgb-window.go new file mode 100644 index 0000000..3944fa4 --- /dev/null +++ b/prototypes/xgb-window.go @@ -0,0 +1,156 @@ +package main + +import ( + "github.com/BurntSushi/xgb" + "github.com/BurntSushi/xgb/xproto" + "log" + "math" + "math/rand" +) + +func main() { + X, err := xgb.NewConn() + if err != nil { + log.Fatalln(err) + } + + setup := xproto.Setup(X) + screen := setup.DefaultScreen(X) + + var visual xproto.Visualid + var depth byte + for _, i := range screen.AllowedDepths { + if i.Depth == 32 { + // TODO: Could/should check other parameters. + for _, v := range i.Visuals { + if v.Class == xproto.VisualClassTrueColor { + visual = v.VisualId + depth = i.Depth + break + } + } + } + } + if visual == 0 { + log.Fatalln("cannot find an RGBA TrueColor visual") + } + + mid, err := xproto.NewColormapId(X) + if err != nil { + log.Fatalln(err) + } + + _ = xproto.CreateColormap( + X, xproto.ColormapAllocNone, mid, screen.Root, visual) + + wid, err := xproto.NewWindowId(X) + if err != nil { + log.Fatalln(err) + } + + // Border pixel and colormap are required when depth differs from parent. + _ = xproto.CreateWindow(X, depth, wid, screen.Root, + 0, 0, 500, 500, 0, xproto.WindowClassInputOutput, + visual, xproto.CwBorderPixel|xproto.CwColormap, + []uint32{0, uint32(mid)}) + + // This could be included in CreateWindow parameters. + _ = xproto.ChangeWindowAttributes(X, wid, + xproto.CwBackPixel|xproto.CwEventMask, []uint32{0x80808080, + xproto.EventMaskStructureNotify | xproto.EventMaskKeyPress | + xproto.EventMaskExposure}) + + title := "Gradient" + _ = xproto.ChangeProperty(X, xproto.PropModeReplace, wid, xproto.AtomWmName, + xproto.AtomString, 8, uint32(len(title)), []byte(title)) + + _ = xproto.MapWindow(X, wid) + + cid, err := xproto.NewGcontextId(X) + if err != nil { + log.Fatalln(err) + } + + _ = xproto.CreateGC(X, cid, xproto.Drawable(wid), + xproto.GcGraphicsExposures, []uint32{0}) + + blend := func(a, b uint32, ratio, gamma float64) uint32 { + iratio := 1 - ratio + + fa := math.Pow(float64(a)/255, gamma) + fb := math.Pow(float64(b)/255, gamma) + + return uint32(math.Pow(ratio*fa+iratio*fb, 1/gamma)*255) & 0xff + } + + // TODO: We could show some text just like we intend to with xgb-xrender.go. + + var w, h uint16 + var start, end uint32 = 0xabcdef, 0x32ab54 + gradient := func() { + ra, ga, ba := (start>>16)&0xff, (start>>8)&0xff, start&0xff + rb, gb, bb := (end>>16)&0xff, (end>>8)&0xff, end&0xff + + var low, high uint16 = 50, h - 50 + if high > h { + return + } + + for y := low; y < high; y++ { + ratio := float64(y-low) / (float64(high) - float64(low)) + + rR := blend(ra, rb, ratio, 2.2) + gG := blend(ga, gb, ratio, 2.2) + bB := blend(ba, bb, ratio, 2.2) + + _ = xproto.ChangeGC(X, cid, xproto.GcForeground, + []uint32{0xff000000 | rR<<16 | gG<<8 | bB}) + _ = xproto.PolyLine(X, xproto.CoordModeOrigin, xproto.Drawable(wid), + cid, []xproto.Point{ + {X: 50, Y: int16(y)}, + {X: int16(w / 2), Y: int16(y)}, + }) + + rR = blend(ra, rb, ratio, 1) + gG = blend(ga, gb, ratio, 1) + bB = blend(ba, bb, ratio, 1) + + _ = xproto.ChangeGC(X, cid, xproto.GcForeground, + []uint32{0xff000000 | rR<<16 | gG<<8 | bB}) + _ = xproto.PolyLine(X, xproto.CoordModeOrigin, xproto.Drawable(wid), + cid, []xproto.Point{ + {X: int16(w / 2), Y: int16(y)}, + {X: int16(w - 50), Y: int16(y)}, + }) + } + } + + for { + ev, xerr := X.WaitForEvent() + if xerr != nil { + log.Printf("Error: %s\n", xerr) + return + } + if ev == nil { + return + } + + log.Printf("Event: %s\n", ev) + switch e := ev.(type) { + case xproto.UnmapNotifyEvent: + return + + case xproto.ConfigureNotifyEvent: + w, h = e.Width, e.Height + + case xproto.KeyPressEvent: + start = rand.Uint32() & 0xffffff + end = rand.Uint32() & 0xffffff + gradient() + + case xproto.ExposeEvent: + gradient() + } + } + +} diff --git a/prototypes/xgb-xrender.go b/prototypes/xgb-xrender.go new file mode 100644 index 0000000..ad768b7 --- /dev/null +++ b/prototypes/xgb-xrender.go @@ -0,0 +1,397 @@ +package main + +// We could also easily use x/image/font/basicfont to load some glyphs into X, +// relying on the fact that it is a vertical array of A8 masks. Though it only +// supports ASCII and has but one size. Best just make a custom BDF loader, +// those fonts have larger coverages and we would be in control. Though again, +// they don't seem to be capable of antialiasing. + +import ( + "fmt" + "github.com/BurntSushi/xgb" + "github.com/BurntSushi/xgb/render" + "github.com/BurntSushi/xgb/xproto" + // golang.org/x/image/font/opentype cannot render yet but the API is + // more or less the same. + "github.com/golang/freetype" + "github.com/golang/freetype/truetype" + "golang.org/x/image/font" + "golang.org/x/image/font/gofont/goregular" + "golang.org/x/image/math/fixed" + "image" + "image/draw" + "log" + "math/rand" +) + +func F64ToFixed(f float64) render.Fixed { return render.Fixed(f * 65536) } +func FixedToF64(f render.Fixed) float64 { return float64(f) / 65536 } + +func glyphListBytes(buf []byte, runes []rune, size int) int { + b := 0 + for _, r := range runes { + switch size { + default: + buf[b] = byte(r) + b += 1 + case 2: + xgb.Put16(buf[b:], uint16(r)) + b += 2 + case 4: + xgb.Put32(buf[b:], uint32(r)) + b += 4 + } + } + return xgb.Pad(b) +} + +// When the len is 255, a GLYPHABLE follows, otherwise a list of CARD8/16/32. +func glyphEltHeaderBytes(buf []byte, len byte, deltaX, deltaY int16) int { + b := 0 + buf[b] = len + b += 4 + xgb.Put16(buf[b:], uint16(deltaX)) + b += 2 + xgb.Put16(buf[b:], uint16(deltaY)) + b += 2 + return xgb.Pad(b) +} + +type xgbCookie interface{ Check() error } + +// TODO: We actually need a higher-level function that also keeps track of +// and loads glyphs into the X server. +// TODO: We also need a way to use kerning tables with this, inserting/removing +// advance pixels between neighboring characters. + +// compositeString makes an appropriate render.CompositeGlyphs request, +// assuming that glyphs equal Unicode codepoints. +func compositeString(c *xgb.Conn, op byte, src, dst render.Picture, + maskFormat render.Pictformat, glyphset render.Glyphset, srcX, srcY int16, + destX, destY int16, text string) xgbCookie { + runes := []rune(text) + + var highest rune + for _, r := range runes { + if r > highest { + highest = r + } + } + + size := 1 + switch { + case highest > 1<<16: + size = 4 + case highest > 1<<8: + size = 2 + } + + // They gave up on the XCB protocol API and we need to serialize explicitly. + + // To spare us from caring about the padding, use the largest number lesser + // than 255 that is divisible by 4 (for size 2 and 4 the requirements are + // less strict but this works in the general case). + const maxPerChunk = 252 + + buf := make([]byte, (len(runes)+maxPerChunk-1)/maxPerChunk*8+len(runes)*size) + b := 0 + + for len(runes) > maxPerChunk { + b += glyphEltHeaderBytes(buf[b:], maxPerChunk, 0, 0) + b += glyphListBytes(buf[b:], runes[:maxPerChunk], size) + runes = runes[maxPerChunk:] + } + if len(runes) > 0 { + b += glyphEltHeaderBytes(buf[b:], byte(len(runes)), destX, destY) + b += glyphListBytes(buf[b:], runes, size) + } + + switch size { + default: + return render.CompositeGlyphs8(c, op, src, dst, maskFormat, glyphset, + srcX, srcY, buf) + case 2: + return render.CompositeGlyphs16(c, op, src, dst, maskFormat, glyphset, + srcX, srcY, buf) + case 4: + return render.CompositeGlyphs32(c, op, src, dst, maskFormat, glyphset, + srcX, srcY, buf) + } +} + +func main() { + X, err := xgb.NewConn() + if err != nil { + log.Fatalln(err) + } + + if err := render.Init(X); err != nil { + log.Fatalln(err) + } + + setup := xproto.Setup(X) + screen := setup.DefaultScreen(X) + + var visual xproto.Visualid + var depth byte + for _, i := range screen.AllowedDepths { + if i.Depth == 32 { + // TODO: Could/should check other parameters. + for _, v := range i.Visuals { + if v.Class == xproto.VisualClassTrueColor { + visual = v.VisualId + depth = i.Depth + break + } + } + } + } + if visual == 0 { + log.Fatalln("cannot find an RGBA TrueColor visual") + } + + mid, err := xproto.NewColormapId(X) + if err != nil { + log.Fatalln(err) + } + + _ = xproto.CreateColormap( + X, xproto.ColormapAllocNone, mid, screen.Root, visual) + + wid, err := xproto.NewWindowId(X) + if err != nil { + log.Fatalln(err) + } + + // Border pixel and colormap are required when depth differs from parent. + _ = xproto.CreateWindow(X, depth, wid, screen.Root, + 0, 0, 500, 500, 0, xproto.WindowClassInputOutput, + visual, xproto.CwBorderPixel|xproto.CwColormap, + []uint32{0, uint32(mid)}) + + // This could be included in CreateWindow parameters. + _ = xproto.ChangeWindowAttributes(X, wid, + xproto.CwBackPixel|xproto.CwEventMask, []uint32{0x80808080, + xproto.EventMaskStructureNotify | xproto.EventMaskKeyPress | + xproto.EventMaskExposure}) + + title := []byte("Gradient") + _ = xproto.ChangeProperty(X, xproto.PropModeReplace, wid, xproto.AtomWmName, + xproto.AtomString, 8, uint32(len(title)), title) + + _ = xproto.MapWindow(X, wid) + + /* + rfilters, err := render.QueryFilters(X, xproto.Drawable(wid)).Reply() + if err != nil { + log.Fatalln(err) + } + + filters := []string{} + for _, f := range rfilters.Filters { + filters = append(filters, f.Name) + } + + log.Printf("filters: %v\n", filters) + */ + + pformats, err := render.QueryPictFormats(X).Reply() + if err != nil { + log.Fatalln(err) + } + + /* + for _, pf := range pformats.Formats { + log.Printf("format %2d: depth %2d, RGBA %3x %3x %3x %3x\n", + pf.Id, pf.Depth, + pf.Direct.RedMask, pf.Direct.GreenMask, pf.Direct.BlueMask, + pf.Direct.AlphaMask) + } + */ + + // Similar to XRenderFindVisualFormat. + // The DefaultScreen is almost certain to be zero. + var pformat render.Pictformat + for _, pd := range pformats.Screens[X.DefaultScreen].Depths { + // This check seems to be slightly extraneous. + if pd.Depth != depth { + continue + } + for _, pv := range pd.Visuals { + if pv.Visual == visual { + pformat = pv.Format + } + } + } + + // ...or just scan through pformats.Formats and look for matches, which is + // what XRenderFindStandardFormat in Xlib does as well as exp/shiny. + + f, err := freetype.ParseFont(goregular.TTF) + if err != nil { + log.Fatalln(err) + } + + // LCD subpixel rendering isn't supported. :( + opts := &truetype.Options{ + Size: 10, + DPI: 96, // TODO: Take this from the screen or monitor. + Hinting: font.HintingFull, + } + face := truetype.NewFace(f, opts) + bounds := f.Bounds(fixed.Int26_6(opts.Size * float64(opts.DPI) * + (64.0 / 72.0))) + + var rgbFormat render.Pictformat + for _, pf := range pformats.Formats { + // Hopefully. Might want to check byte order. + if pf.Depth == 32 && pf.Direct.AlphaMask != 0 { + rgbFormat = pf.Id + break + } + } + + gsid, err := render.NewGlyphsetId(X) + if err != nil { + log.Fatalln(err) + } + + // NOTE: A depth of 24 will not work, the server always rejects it. + // Composite alpha doesn't make sense since golang/freetype can't use it. + // We use RGBA here just so that lines are padded to 32 bits. + _ = render.CreateGlyphSet(X, gsid, rgbFormat) + + // NOTE: We could do gamma post-correction in higher precision if we + // implemented our own clone of the image.Image implementation. + nrgb := image.NewRGBA(image.Rect( + +bounds.Min.X.Floor(), + -bounds.Min.Y.Floor(), + +bounds.Max.X.Ceil(), + -bounds.Max.Y.Ceil(), + )) + + for r := rune(32); r < 128; r++ { + dr, mask, maskp, advance, ok := face.Glyph( + fixed.P(0, 0) /* subpixel destination location */, r) + if !ok { + log.Println("skip") + continue + } + + for i := 0; i < len(nrgb.Pix); i++ { + nrgb.Pix[i] = 0 + } + + draw.Draw(nrgb, dr, mask, maskp, draw.Src) + + _ = render.AddGlyphs(X, gsid, 1, []uint32{uint32(r)}, + []render.Glyphinfo{{ + Width: uint16(nrgb.Rect.Size().X), + Height: uint16(nrgb.Rect.Size().Y), + X: int16(-bounds.Min.X.Floor()), + Y: int16(+bounds.Max.Y.Ceil()), + XOff: int16(advance.Ceil()), + YOff: int16(0), + }}, []byte(nrgb.Pix)) + } + + pid, err := render.NewPictureId(X) + if err != nil { + log.Fatalln(err) + } + + // Dithering is not supported. :( + render.CreatePicture(X, pid, xproto.Drawable(wid), pformat, 0, []uint32{}) + + // Reserve an ID for the gradient. + gid, err := render.NewPictureId(X) + if err != nil { + log.Fatalln(err) + } + + whiteid, err := render.NewPictureId(X) + if err != nil { + log.Fatalln(err) + } + + _ = render.CreateSolidFill(X, whiteid, render.Color{ + Red: 0xffff, + Green: 0xffff, + Blue: 0xffff, + Alpha: 0xffff, + }) + + var from, to render.Color + var start, end uint32 + recolor := func() { + start = rand.Uint32() & 0xffffff + from = render.Color{ + Red: 0x101 * uint16((start>>16)&0xff), + Green: 0x101 * uint16((start>>8)&0xff), + Blue: 0x101 * uint16(start&0xff), + Alpha: 0xffff, + } + + end = rand.Uint32() & 0xffffff + to = render.Color{ + Red: 0x101 * uint16((end>>16)&0xff), + Green: 0x101 * uint16((end>>8)&0xff), + Blue: 0x101 * uint16(end&0xff), + Alpha: 0xffff, + } + } + + var w, h uint16 + gradient := func() { + if w < 100 || h < 100 { + return + } + + // We could also use a transformation matrix for changes in size. + _ = render.CreateLinearGradient(X, gid, + render.Pointfix{F64ToFixed(0), F64ToFixed(0)}, + render.Pointfix{F64ToFixed(0), F64ToFixed(float64(h) - 100)}, + 2, []render.Fixed{F64ToFixed(0), F64ToFixed(1)}, + []render.Color{from, to}) + + _ = render.Composite(X, render.PictOpSrc, gid, render.PictureNone, pid, + 0, 0, 0, 0, 50, 50, w-100, h-100) + + _ = render.FreePicture(X, gid) + + _ = compositeString(X, render.PictOpOver, whiteid, pid, + 0 /* TODO: mask Pictureformat? */, gsid, 0, 0, 100, 100, + fmt.Sprintf("%#06x - %#06x", start, end)) + _ = compositeString(X, render.PictOpOver, whiteid, pid, + 0 /* TODO: mask Pictureformat? */, gsid, 0, 0, 100, 150, + "The quick brown fox jumps over the lazy dog.") + } + + for { + ev, xerr := X.WaitForEvent() + if xerr != nil { + log.Printf("Error: %s\n", xerr) + return + } + if ev == nil { + return + } + + log.Printf("Event: %s\n", ev) + switch e := ev.(type) { + case xproto.UnmapNotifyEvent: + return + + case xproto.ConfigureNotifyEvent: + w, h = e.Width, e.Height + recolor() + + case xproto.KeyPressEvent: + recolor() + gradient() + + case xproto.ExposeEvent: + gradient() + } + } +} |