diff options
| -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. +}  | 
