package main import ( "janouch.name/haven/nexgb" "janouch.name/haven/nexgb/render" "janouch.name/haven/nexgb/xproto" "github.com/golang/freetype" "github.com/golang/freetype/truetype" "golang.org/x/image/font" "golang.org/x/image/font/gofont/gomono" "golang.org/x/image/math/fixed" //"golang.org/x/image/font/gofont/gomonobold" //"golang.org/x/image/font/gofont/gomonoitalic" //"golang.org/x/image/font/gofont/gomonobolditalic" "flag" "image" "image/color" "image/draw" "log" "os" "strings" ) // #include // #include //import "C" // ----------------------------------------------------------------------------- // Text rendering // glyphListBytes serializes a run of runes for use in an X11 request. 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: nexgb.Put16(buf[b:], uint16(r)) b += 2 case 4: nexgb.Put32(buf[b:], uint32(r)) b += 4 } } return nexgb.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 nexgb.Put16(buf[b:], uint16(deltaX)) b += 2 nexgb.Put16(buf[b:], uint16(deltaY)) b += 2 return nexgb.Pad(b) } type xgbCookie interface{ Check() error } // compositeString makes an appropriate render.CompositeGlyphs request, // assuming that glyphs equal Unicode codepoints. func compositeString(c *nexgb.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 glyphs buf *image.RGBA // rendering buffer X *nexgb.Conn gsid render.Glyphset loaded map[rune]bool } func newTextRenderer(X *nexgb.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(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) } // ----------------------------------------------------------------------------- // Keys //go:generate sh -c "./gen-keysyms.sh > keysyms.go" //go:generate sh -c "./gen-unicode-map.sh > unicode_map.go" type keyMapper struct { X *nexgb.Conn setup *xproto.SetupInfo mapping *xproto.GetKeyboardMappingReply modeSwitchMask uint16 } func newKeyMapper(X *nexgb.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 == XK_Mode_switch { 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. // TODO: Now that we have scripts for the mapping, finish this. 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 } // ----------------------------------------------------------------------------- // Utilities func getShell() string { shell := os.Getenv("SHELL") if shell == "" { // TODO: Find out another way, os/user won't give it to us, // easiest to just use Cgo again like it does internally. shell = "/bin/sh" } return shell } // ----------------------------------------------------------------------------- // Program //go:generate sh -c "./gen-rune-width.sh > rune_width.go" type cell struct { // TODO: We want to NFC-normalize the runes. // https://blog.golang.org/normalization runes []rune // TODO: attributes fg, bg color.Color } type row struct { cells []cell } const ( winPadding = 5 ) var ( scrollback []row scroll int // index of the top line curX, curY int // cursor position selX0, selY0 int // selection start selX1, selY1 int // selection end wid xproto.Window winW, winH uint16 // inner window size pid, blackid render.Picture X *nexgb.Conn setup *xproto.SetupInfo screen *xproto.ScreenInfo tr *textRenderer km *keyMapper ) func initFont() { trOptions := truetype.Options{ Size: 10, DPI: float64(screen.WidthInPixels) / float64(screen.WidthInMillimeters) * 25.4, Hinting: font.HintingFull, } // TODO: We will also want to make renderers for bold, italic, bold+italic. var err error tr, err = newTextRenderer(X, gomono.TTF, &trOptions) if err != nil { log.Fatalln(err) } } func initWindow(name string) { var err error wid, err = xproto.NewWindowId(X) if err != nil { log.Fatalln(err) } // Compute window size according to our font. cellW := tr.bounds.Max.X.Ceil() - tr.bounds.Min.X.Floor() cellH := tr.bounds.Max.Y.Ceil() - tr.bounds.Min.Y.Floor() winW = 80 * uint16(cellW) winH = 24 * uint16(cellH) _ = xproto.CreateWindow(X, screen.RootDepth, wid, screen.Root, 0, 0, winW, winH, 0, xproto.WindowClassInputOutput, screen.RootVisual, xproto.CwBackPixel|xproto.CwEventMask, []uint32{0xffffffff, /* xproto.EventMaskButtonPress | xproto.EventMaskButtonMotion | xproto.EventMaskButtonRelease | */ xproto.EventMaskStructureNotify | xproto.EventMaskExposure | xproto.EventMaskKeyPress}) title := []byte(name) _ = xproto.ChangeProperty(X, xproto.PropModeReplace, wid, xproto.AtomWmName, xproto.AtomString, 8, uint32(len(title)), title) _ = xproto.MapWindow(X, wid) } func initRender() { pformats, err := render.QueryPictFormats(X).Reply() if err != nil { log.Fatalln(err) } var pformat render.Pictformat VisualFormat: for _, pd := range pformats.Screens[X.DefaultScreen].Depths { for _, pv := range pd.Visuals { if pv.Visual == screen.RootVisual { pformat = pv.Format break VisualFormat } } } if pid, err = render.NewPictureId(X); err != nil { log.Fatalln(err) } _ = render.CreatePicture(X, pid, xproto.Drawable(wid), pformat, 0, []uint32{}) if blackid, err = render.NewPictureId(X); err != nil { log.Fatalln(err) } _ = render.CreateSolidFill(X, blackid, render.Color{Alpha: 0xffff}) } func initKeyMapper() { var err error if km, err = newKeyMapper(X); err != nil { log.Fatalln(err) } } func redraw() { y, ascent, step := winPadding, tr.bounds.Max.Y.Ceil(), tr.bounds.Max.Y.Ceil()-tr.bounds.Min.Y.Floor() for _, line := range scrollback[scroll:] { if uint16(y) >= winH { break } text := "" for _, c := range line.cells { for _, r := range c.runes { text = text + string(r) } } _ = tr.render(blackid, pid, 0, 0, winPadding, int16(y+ascent), text) y += step } vis := float64(winH-2*winPadding) / float64(step) if vis < float64(len(scrollback)) { length := float64(step) * (vis + 1) * vis / float64(len(scrollback)) start := float64(step) * float64(scroll) * vis / float64(len(scrollback)) _ = render.FillRectangles(X, render.PictOpSrc, pid, render.Color{Alpha: 0xffff}, []xproto.Rectangle{{ X: int16(winW - 2*winPadding), Y: int16(start), Width: 2 * winPadding, Height: uint16(length + 2*winPadding)}}) } } func onResize(width, height uint16) { winW, winH = width, height // We should receive an ExposeEvent soon. // TODO: Track the bottom of the virtual terminal screen. // - Adjust the top line index, if necessary (screen got higher). } func write(r rune) { // TODO: Write over the current scrollback position, // with the current attributes. // TODO: Make space in the scrollback so that we have somewhere to write to. // TODO: Cause a redraw. // - How to invalidate a region? } func onKeyPress(ks xproto.Keysym) { switch ks { case XK_Escape: // Just as an example, this doesn't make sense anymore. return default: if r := KeysymToRune(ks); r > 0 { write(r) } } } func handleEvent(ev nexgb.Event) bool { switch e := ev.(type) { case xproto.UnmapNotifyEvent: return false case xproto.ConfigureNotifyEvent: onResize(e.Width, e.Height) case xproto.MappingNotifyEvent: _ = km.update() case xproto.KeyPressEvent: onKeyPress(km.decode(e)) case xproto.ButtonPressEvent: switch e.Detail { case xproto.ButtonIndex4: if scroll > 0 { scroll-- } case xproto.ButtonIndex5: if scroll+1 < len(scrollback) { scroll++ } } case xproto.ExposeEvent: // TODO: If the count is non-zero, just add rectangles to // a structure and flush it later. But maybe this doesn't make // any sense anymore with today's compositing. if e.Count == 0 { redraw() } } return true } func main() { flag.Parse() shell := getShell() if flag.NArg() > 0 { shell = flag.Arg(0) } var err error if X, err = nexgb.NewConn(); err != nil { log.Fatalln(err) } if err := render.Init(X); err != nil { log.Fatalln(err) } setup = xproto.Setup(X) screen = setup.DefaultScreen(X) if screen.RootDepth < 24 { log.Fatalln("need more colors") } initFont() initWindow(shell) initRender() initKeyMapper() // TODO: Set normal hints so that the window is resized in steps. // https://www.x.org/releases/X11R7.7/doc/xorg-docs/icccm/icccm.html // xgbutils seems to contain code (WmNormalHintsSet) // - The xgbutil.XUtil object is only used for xprop functions. // - Seems like I want to import half of xgbutil. // // "Window managers are encouraged to use i and j instead of width and // height in reporting window sizes to users"! for { ev, xerr := X.WaitForEvent() if xerr != nil { log.Printf("Error: %s\n", xerr) return } if ev != nil && !handleEvent(ev) { break } } // TODO: Any post-close cleanup. }