aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPřemysl Eric Janouch <p@janouch.name>2020-08-11 00:43:07 +0200
committerPřemysl Eric Janouch <p@janouch.name>2022-03-20 23:28:53 +0100
commit6d6aa2171050580823073aed8d4f18d91fe37aa7 (patch)
tree5a77eafad3b8c45715d6d33b4bb0278459a67547
parent442fa5d6607d6188be01d342e1bc7d24b977bd22 (diff)
downloadhaven-6d6aa2171050580823073aed8d4f18d91fe37aa7.tar.gz
haven-6d6aa2171050580823073aed8d4f18d91fe37aa7.tar.xz
haven-6d6aa2171050580823073aed8d4f18d91fe37aa7.zip
WIP: ht: add main source file
Initial commit of sorts.
-rw-r--r--ht/main.go615
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.
+}