From 6d6aa2171050580823073aed8d4f18d91fe37aa7 Mon Sep 17 00:00:00 2001
From: Přemysl Eric Janouch 
Date: Tue, 11 Aug 2020 00:43:07 +0200
Subject: WIP: ht: add main source file
Initial commit of sorts.
---
 ht/main.go | 615 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 615 insertions(+)
 create mode 100644 ht/main.go
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 
+// #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.
+}
+
+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.
+}
-- 
cgit v1.2.3-70-g09d2