diff options
Diffstat (limited to 'ht/main.go')
-rw-r--r-- | ht/main.go | 615 |
1 files changed, 615 insertions, 0 deletions
diff --git a/ht/main.go b/ht/main.go new file mode 100644 index 0000000..95938d8 --- /dev/null +++ b/ht/main.go @@ -0,0 +1,615 @@ +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 <fcntl.h> +// #include <stdlib.h> +//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. +} + +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 { + } + } +} + +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. +} |