diff options
| author | Calvin Morrison <calvin@pobox.com> | 2026-08-22 13:16:35 -0400 |
|---|---|---|
| committer | Calvin Morrison <calvin@pobox.com> | 2026-08-22 13:16:35 -0400 |
| commit | 9aacce8b3b54060d0037eca897856d7273e6e5e8 (patch) | |
| tree | a98ea0cb151a7ded56fe681d6782986b2807dfcf | |
| parent | 5ef4699d05bc919255449a9af780f216a0589a72 (diff) | |
gui: a /dev/draw layer in go, with no cgo and no devdraw
9fans.net/go/draw builds for plan9 but shells out to plan9port's
devdraw, which 9front does not have. This talks to /dev/draw itself:
the protocol is file i/o, so a pure-go client is a few hundred lines
under an already-complete idea.
draw/keyboard.go opens /dev/cons and turns the console raw, as
libdraw's initkeyboard does. Without it rio keeps its line editor on
the window and paints what you type over the drawing.
draw/snarf.go writes /dev/snarf, which is what every other program on
the system means by copy.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| -rw-r--r-- | gui/.gitignore | 3 | ||||
| -rw-r--r-- | gui/cmd/hello/main.go | 5 | ||||
| -rw-r--r-- | gui/cmd/spike/main.go | 106 | ||||
| -rw-r--r-- | gui/cmd/text/main.go | 66 | ||||
| -rw-r--r-- | gui/draw/draw.go | 250 | ||||
| -rw-r--r-- | gui/draw/font.go | 336 | ||||
| -rw-r--r-- | gui/draw/keyboard.go | 65 | ||||
| -rw-r--r-- | gui/draw/mouse.go | 74 | ||||
| -rw-r--r-- | gui/draw/snarf.go | 15 | ||||
| -rw-r--r-- | gui/draw/window.go | 86 | ||||
| -rw-r--r-- | gui/go.mod | 3 | ||||
| -rw-r--r-- | gui/probe/go.mod | 3 | ||||
| -rw-r--r-- | gui/probe/main.go | 5 |
13 files changed, 1017 insertions, 0 deletions
diff --git a/gui/.gitignore b/gui/.gitignore new file mode 100644 index 0000000..b10d965 --- /dev/null +++ b/gui/.gitignore @@ -0,0 +1,3 @@ +# cross-compiled guest binaries +*.9 +probe/hello-go1.* diff --git a/gui/cmd/hello/main.go b/gui/cmd/hello/main.go new file mode 100644 index 0000000..26b925b --- /dev/null +++ b/gui/cmd/hello/main.go @@ -0,0 +1,5 @@ +package main + +import "fmt" + +func main() { fmt.Println("hello from go on plan9") } diff --git a/gui/cmd/spike/main.go b/gui/cmd/spike/main.go new file mode 100644 index 0000000..442df52 --- /dev/null +++ b/gui/cmd/spike/main.go @@ -0,0 +1,106 @@ +// Spike: prove that a Plan 9 GUI can be written in Go with nothing but the +// standard library -- no C, no cgo, no libdraw, no window system. +// +// Draws a panel layout and a cursor block that tracks the mouse. +// Button 3 quits; so does the deadline, so a forgotten instance on a VM +// does not sit on /dev/draw forever. +package main + +import ( + "fmt" + "os" + "time" + + "9front/gui/draw" +) + +func main() { + if err := run(); err != nil { + fmt.Fprintf(os.Stderr, "spike: %v\n", err) + os.Exit(1) + } +} + +func run() error { + d, err := draw.Init("/dev") + if err != nil { + return err + } + defer d.Close() + if d.Screen == nil { + return fmt.Errorf("draw device gave no screen image") + } + scr := d.Screen + r := scr.Rect() + fmt.Fprintf(os.Stderr, "spike: screen %dx%d\n", r.Dx(), r.Dy()) + + col := map[string]uint32{} + img := map[string]*draw.Image{} + for name, v := range map[string]uint32{ + "bg": 0x2B3A42FF, + "panel": 0x3F5765FF, + "accent": 0xBDD4DEFF, + "hot": 0xEFA00BFF, + } { + col[name] = v + if img[name], err = d.Color(v); err != nil { + return err + } + } + + // Static layout: a header bar and three panels, laid out by arithmetic + // rather than by hand-placed constants. This is the seam where a real + // layout pass would go. + repaint := func() { + draw.Draw(scr, r, img["bg"], nil, draw.ZP) + hdr := draw.Rect(r.Min.X, r.Min.Y, r.Max.X, r.Min.Y+40) + draw.Draw(scr, hdr, img["accent"], nil, draw.ZP) + + const gap = 12 + top := r.Min.Y + 40 + gap + w := (r.Dx() - 4*gap) / 3 + for i := int32(0); i < 3; i++ { + x := r.Min.X + gap + i*(w+gap) + p := draw.Rect(x, top, x+w, r.Max.Y-gap) + draw.Draw(scr, p, img["panel"], nil, draw.ZP) + } + } + repaint() + d.Flush() + + mc, err := draw.OpenMouse("/dev") + if err != nil { + return err + } + defer mc.Close() + + deadline := time.After(10 * time.Minute) + cursor := draw.Rect(0, 0, 28, 28) + var last draw.Rectangle + for { + select { + case m, ok := <-mc.C: + if !ok { + return nil + } + if m.Buttons&4 != 0 { + return nil + } + // Damage repair without a full repaint: erase where the + // block was, then draw it where the mouse is now. + if last != (draw.Rectangle{}) { + repaint() + } + now := cursor.Add(draw.Point{X: m.X - 14, Y: m.Y - 14}) + draw.Draw(scr, now, img["hot"], nil, draw.ZP) + d.Flush() + last = now + case <-mc.Resize: + r = scr.Rect() + repaint() + d.Flush() + case <-deadline: + return nil + } + } +} diff --git a/gui/cmd/text/main.go b/gui/cmd/text/main.go new file mode 100644 index 0000000..f1e2a9f --- /dev/null +++ b/gui/cmd/text/main.go @@ -0,0 +1,66 @@ +// text: prove the font layer draws glyphs on the raw screen. +package main + +import ( + "fmt" + "os" + "time" + + "9front/gui/draw" +) + +const fontpath = "/lib/font/bit/lucidasans/unicode.8.font" + +func main() { + if err := run(); err != nil { + fmt.Fprintf(os.Stderr, "text: %v\n", err) + os.Exit(1) + } +} + +func run() error { + d, err := draw.Init("/dev") + if err != nil { + return err + } + defer d.Close() + scr := d.Screen + r := scr.Rect() + + f, err := d.OpenFont(fontpath) + if err != nil { + return err + } + fmt.Fprintf(os.Stderr, "text: font h=%d ascent=%d\n", f.Height, f.Ascent) + + // acme's palette: a 25% mix over white, per allocimagemix. + bg, err := d.Color(0xFFFFEAFF) + if err != nil { + return err + } + ink, err := d.Color(0x000000FF) + if err != nil { + return err + } + rule, err := d.Color(0x99994CFF) + if err != nil { + return err + } + + draw.Draw(scr, r, bg, nil, draw.ZP) + y := r.Min.Y + 20 + for _, s := range []string{ + "Go on Plan 9: glyphs from /lib/font/bit, no libdraw.", + "ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz", + "0123456789 !@#$%^&*()-=[]{};'\\:\"|,./<>?", + "width(\"hello\") = " + fmt.Sprint(f.Width("hello")) + " px", + time.Now().Format("Mon 2 Jan 2006 15:04:05"), + } { + f.String(scr, draw.Point{X: r.Min.X + 20, Y: y}, ink, s) + y += int32(f.Height) + 6 + } + draw.Draw(scr, draw.Rect(r.Min.X+20, y+8, r.Min.X+520, y+9), rule, nil, draw.ZP) + d.Flush() + time.Sleep(90 * time.Second) + return nil +} diff --git a/gui/draw/draw.go b/gui/draw/draw.go new file mode 100644 index 0000000..0aa7911 --- /dev/null +++ b/gui/draw/draw.go @@ -0,0 +1,250 @@ +// Package draw speaks the Plan 9 /dev/draw protocol directly. +// +// There is no C, no cgo and no libdraw here: /dev/draw is a file, its +// protocol is a handful of little-endian messages, and this file is the +// whole of what libdraw's init.c, alloc.c and draw.c actually do. +// See draw(3) and /sys/src/libdraw for the authority. +package draw + +import ( + "encoding/binary" + "fmt" + "os" + "strconv" + "sync" +) + +// Channel descriptors, from /sys/include/draw.h. +const ( + chGrey1 = 0x31 + chRGB24 = 0x081828 + chRGBA32 = 0x08182848 +) + +type Point struct{ X, Y int32 } + +type Rectangle struct{ Min, Max Point } + +func Rect(x0, y0, x1, y1 int32) Rectangle { + return Rectangle{Point{x0, y0}, Point{x1, y1}} +} + +func (r Rectangle) Dx() int32 { return r.Max.X - r.Min.X } +func (r Rectangle) Dy() int32 { return r.Max.Y - r.Min.Y } + +// Add offsets a rectangle by p. +func (r Rectangle) Add(p Point) Rectangle { + return Rectangle{ + Point{r.Min.X + p.X, r.Min.Y + p.Y}, + Point{r.Max.X + p.X, r.Max.Y + p.Y}, + } +} + +var ZP Point + +// Colors are RGBA, as draw(2) writes them. +const ( + White = 0xFFFFFFFF + Black = 0x000000FF + Red = 0xFF0000FF + Green = 0x00FF00FF + Blue = 0x0000FFFF +) + +type Image struct { + d *Display + id uint32 + R Rectangle + Clipr Rectangle + chn uint32 + repl bool +} + +func (i *Image) Rect() Rectangle { return i.R } + +type Display struct { + ctl, data *os.File + dirno int + + // Screen is the display's own image, id 0. Without a window system + // that is the physical screen; under rio it is the whole screen and + // the window comes from /dev/winname instead. + Screen *Image + + // opaque is libdraw's display->opaque: a replicated all-ones GREY1 + // pixel. Draw with a nil mask means "mask with this", not "mask with + // image 0" -- image 0 is the screen. + opaque *Image + + mu sync.Mutex + nextid uint32 +} + +// Init attaches to the draw device under dev (normally "/dev"). +func Init(dev string) (*Display, error) { + if dev == "" { + dev = "/dev" + } + ctl, err := os.OpenFile(dev+"/draw/new", os.O_RDWR, 0) + if err != nil { + return nil, fmt.Errorf("open draw/new: %w", err) + } + // 12 fields of 12 bytes: id, imageid, chan, repl, r[4], clipr[4]. + var info [12 * 12]byte + n, err := ctl.Read(info[:]) + if err != nil || n < 12 { + ctl.Close() + return nil, fmt.Errorf("read draw/new: short read %d: %w", n, err) + } + fld := func(i int) int32 { + s := string(info[i*12 : i*12+12]) + v, _ := strconv.Atoi(trim(s)) + return int32(v) + } + d := &Display{ctl: ctl, dirno: int(fld(0))} + data, err := os.OpenFile(fmt.Sprintf("%s/draw/%d/data", dev, d.dirno), os.O_RDWR, 0) + if err != nil { + ctl.Close() + return nil, fmt.Errorf("open draw/%d/data: %w", d.dirno, err) + } + d.data = data + if n >= len(info) { + d.Screen = &Image{ + d: d, + id: 0, + chn: strToChan(trim(string(info[2*12 : 3*12]))), + repl: fld(3) != 0, + R: Rect(fld(4), fld(5), fld(6), fld(7)), + Clipr: Rect(fld(8), fld(9), fld(10), fld(11)), + } + } + if d.opaque, err = d.Alloc(Rect(0, 0, 1, 1), chGrey1, true, White); err != nil { + d.Close() + return nil, err + } + return d, nil +} + +func (d *Display) Close() error { + d.data.Close() + return d.ctl.Close() +} + +func trim(s string) string { + i, j := 0, len(s) + for i < j && s[i] == ' ' { + i++ + } + for j > i && (s[j-1] == ' ' || s[j-1] == '\n') { + j-- + } + return s[i:j] +} + +// strToChan parses "r8g8b8" and friends. Only what the screen actually +// reports is needed, so unknown names fall back to RGB24. +func strToChan(s string) uint32 { + typ := map[byte]uint32{'r': 0, 'g': 1, 'b': 2, 'k': 3, 'a': 4, 'm': 5, 'x': 6} + var c uint32 + for i := 0; i+1 < len(s); i += 2 { + t, ok := typ[s[i]] + if !ok { + return chRGB24 + } + nb := uint32(s[i+1] - '0') + c = c<<8 | (t&15)<<4 | nb&15 + } + if c == 0 { + return chRGB24 + } + return c +} + +func put32(b []byte, off int, v uint32) { + binary.LittleEndian.PutUint32(b[off:], v) +} + +func (d *Display) write(msg []byte) error { + d.mu.Lock() + defer d.mu.Unlock() + _, err := d.data.Write(msg) + return err +} + +// Alloc creates a new image on the server. A 1x1 replicated image is how +// you make a solid colour: it tiles to fill whatever you draw it into. +func (d *Display) Alloc(r Rectangle, chn uint32, repl bool, col uint32) (*Image, error) { + d.mu.Lock() + d.nextid++ + id := d.nextid + d.mu.Unlock() + + clipr := r + if repl { + // Huge but not infinite, so offsets stay huge instead of overflowing. + clipr = Rect(-0x3FFFFFFF, -0x3FFFFFFF, 0x3FFFFFFF, 0x3FFFFFFF) + } + b := make([]byte, 51) + b[0] = 'b' + put32(b, 1, id) + put32(b, 5, 0) // screenid: not a window + b[9] = 0 // refresh: Refbackup + put32(b, 10, chn) + if repl { + b[14] = 1 + } + put32(b, 15, uint32(r.Min.X)) + put32(b, 19, uint32(r.Min.Y)) + put32(b, 23, uint32(r.Max.X)) + put32(b, 27, uint32(r.Max.Y)) + put32(b, 31, uint32(clipr.Min.X)) + put32(b, 35, uint32(clipr.Min.Y)) + put32(b, 39, uint32(clipr.Max.X)) + put32(b, 43, uint32(clipr.Max.Y)) + put32(b, 47, col) + if err := d.write(b); err != nil { + return nil, fmt.Errorf("alloc image: %w", err) + } + return &Image{d: d, id: id, R: r, Clipr: clipr, chn: chn, repl: repl}, nil +} + +// Color is the common case of Alloc: one replicated pixel of a solid colour. +func (d *Display) Color(col uint32) (*Image, error) { + return d.Alloc(Rect(0, 0, 1, 1), chRGBA32, true, col) +} + +// Free releases the image. All of a client's images go away by themselves +// when its data fd closes, so this is only for long-running programs. +func (i *Image) Free() error { + b := make([]byte, 5) + b[0] = 'f' + put32(b, 1, i.id) + return i.d.write(b) +} + +// Draw copies src (through mask, or opaquely if mask is nil) into r on dst. +func Draw(dst *Image, r Rectangle, src *Image, mask *Image, p Point) error { + b := make([]byte, 45) + b[0] = 'd' + put32(b, 1, dst.id) + put32(b, 5, src.id) + if mask == nil { + mask = dst.d.opaque + } + put32(b, 9, mask.id) + put32(b, 13, uint32(r.Min.X)) + put32(b, 17, uint32(r.Min.Y)) + put32(b, 21, uint32(r.Max.X)) + put32(b, 25, uint32(r.Max.Y)) + put32(b, 29, uint32(p.X)) + put32(b, 33, uint32(p.Y)) + put32(b, 37, uint32(p.X)) + put32(b, 41, uint32(p.Y)) + return dst.d.write(b) +} + +// Flush makes queued drawing visible. devdraw executes each write as it +// arrives, so this only matters for the screen refresh. +func (d *Display) Flush() error { + return d.write([]byte{'v'}) +} diff --git a/gui/draw/font.go b/gui/draw/font.go new file mode 100644 index 0000000..b94f2fd --- /dev/null +++ b/gui/draw/font.go @@ -0,0 +1,336 @@ +package draw + +import ( + "bufio" + "fmt" + "io" + "os" + "path" + "strconv" + "strings" +) + +// Fontchar is one glyph's metrics, unpacked from the 6-byte on-disk form. +type Fontchar struct { + X int32 // left edge in the subfont image + Top, Bottom, Left uint8 + Width uint8 // advance +} + +// Subfont is one image of glyphs plus their metrics -- the unit a .font file +// is assembled from. +type Subfont struct { + Bits *Image + N int + Height, Ascent int + Info []Fontchar // N+1 entries; the last one bounds the last glyph +} + +type frange struct { + min, max rune + offset int + file string + sub *Subfont // loaded on first use +} + +// Font is a .font file: a height, an ascent, and a set of rune ranges each +// served by a subfont. +type Font struct { + d *Display + Name string + Height, Ascent int + ranges []frange +} + +// OpenFont reads a .font file. Subfonts are loaded lazily, because a unicode +// font names dozens of them and a calendar touches two. +func (d *Display) OpenFont(name string) (*Font, error) { + f, err := os.Open(name) + if err != nil { + return nil, fmt.Errorf("open font: %w", err) + } + defer f.Close() + + fnt := &Font{d: d, Name: name} + dir := path.Dir(name) + sc := bufio.NewScanner(f) + first := true + for sc.Scan() { + fld := strings.Fields(sc.Text()) + if len(fld) == 0 { + continue + } + if first { + if len(fld) < 2 { + return nil, fmt.Errorf("font %s: bad first line", name) + } + h, err1 := strconv.ParseInt(fld[0], 0, 32) + a, err2 := strconv.ParseInt(fld[1], 0, 32) + if err1 != nil || err2 != nil || h <= 0 || a <= 0 { + return nil, fmt.Errorf("font %s: bad height/ascent", name) + } + fnt.Height, fnt.Ascent = int(h), int(a) + first = false + continue + } + // min max [offset] file + if len(fld) < 3 { + continue + } + min, err1 := strconv.ParseInt(fld[0], 0, 32) + max, err2 := strconv.ParseInt(fld[1], 0, 32) + if err1 != nil || err2 != nil || min > max { + return nil, fmt.Errorf("font %s: bad range %q", name, sc.Text()) + } + r := frange{min: rune(min), max: rune(max)} + rest := fld[2:] + // The offset is optional and sits before the filename, so it is + // only an offset if it parses as a number and something follows. + if len(rest) >= 2 { + if off, err := strconv.ParseInt(rest[0], 0, 32); err == nil { + r.offset = int(off) + rest = rest[1:] + } + } + r.file = rest[0] + if !path.IsAbs(r.file) { + r.file = path.Join(dir, r.file) + } + fnt.ranges = append(fnt.ranges, r) + } + if err := sc.Err(); err != nil { + return nil, err + } + if first { + return nil, fmt.Errorf("font %s: empty", name) + } + return fnt, nil +} + +// lookup finds the subfont holding r and r's index within it. +func (f *Font) lookup(r rune) (*Subfont, int) { + for i := range f.ranges { + g := &f.ranges[i] + if r < g.min || r > g.max { + continue + } + if g.sub == nil { + s, err := f.d.readSubfont(g.file) + if err != nil { + return nil, 0 + } + g.sub = s + } + n := int(r-g.min) + g.offset + if n < 0 || n >= g.sub.N { + return nil, 0 + } + return g.sub, n + } + return nil, 0 +} + +// String draws s at p (p is the top-left, as in libdraw) in colour src, and +// returns the point just past the last glyph. +func (f *Font) String(dst *Image, p Point, src *Image, s string) Point { + for _, r := range s { + sub, n := f.lookup(r) + if sub == nil { + continue + } + i, i1 := sub.Info[n], sub.Info[n+1] + w := i1.X - i.X + if w > 0 { + Draw(dst, + Rect(p.X+int32(i.Left), p.Y+int32(i.Top), + p.X+int32(i.Left)+w, p.Y+int32(i.Bottom)), + src, sub.Bits, Point{i.X, int32(i.Top)}) + } + p.X += int32(i.Width) + } + return p +} + +// Width is the advance of s, without drawing it. +func (f *Font) Width(s string) int32 { + var w int32 + for _, r := range s { + sub, n := f.lookup(r) + if sub == nil { + continue + } + w += int32(sub.Info[n].Width) + } + return w +} + +// readSubfont loads one subfont file: a Plan 9 image, then the glyph table. +// +// The image is never decompressed here. Plan 9's compressed image format is +// exactly what the draw device's 'Y' message accepts, so the blocks go +// straight to the kernel -- which is all libdraw's creadimage does too. +func (d *Display) readSubfont(name string) (*Subfont, error) { + f, err := os.Open(name) + if err != nil { + return nil, fmt.Errorf("open subfont: %w", err) + } + defer f.Close() + r := bufio.NewReader(f) + + var hdr [5 * 12]byte + if _, err := io.ReadFull(r, hdr[:11]); err != nil { + return nil, fmt.Errorf("%s: short header: %w", name, err) + } + compressed := string(hdr[:11]) == "compressed\n" + if compressed { + if _, err := io.ReadFull(r, hdr[:]); err != nil { + return nil, fmt.Errorf("%s: short header: %w", name, err) + } + } else if _, err := io.ReadFull(r, hdr[11:]); err != nil { + return nil, fmt.Errorf("%s: short header: %w", name, err) + } + + fld := func(i int) int32 { + v, _ := strconv.Atoi(trim(string(hdr[i*12 : i*12+12]))) + return int32(v) + } + chn := strToChan(trim(string(hdr[0:12]))) + ir := Rect(fld(1), fld(2), fld(3), fld(4)) + img, err := d.Alloc(ir, chn, false, 0) + if err != nil { + return nil, fmt.Errorf("%s: %w", name, err) + } + + if compressed { + err = img.loadCompressed(r, ir) + } else { + err = img.loadRaw(r, ir, chn) + } + if err != nil { + return nil, fmt.Errorf("%s: %w", name, err) + } + + // n, height, ascent, then 6 bytes per glyph for n+1 glyphs. + var sh [3 * 12]byte + if _, err := io.ReadFull(r, sh[:]); err != nil { + return nil, fmt.Errorf("%s: short subfont header: %w", name, err) + } + n, _ := strconv.Atoi(trim(string(sh[0:12]))) + if n <= 0 || n > 0x7fff { + return nil, fmt.Errorf("%s: bad glyph count %d", name, n) + } + sf := &Subfont{ + Bits: img, + N: n, + Height: mustAtoi(trim(string(sh[12:24]))), + Ascent: mustAtoi(trim(string(sh[24:36]))), + Info: make([]Fontchar, n+1), + } + pk := make([]byte, 6*(n+1)) + if _, err := io.ReadFull(r, pk); err != nil { + return nil, fmt.Errorf("%s: short glyph table: %w", name, err) + } + for i := 0; i <= n; i++ { + b := pk[i*6:] + sf.Info[i] = Fontchar{ + X: int32(b[0]) | int32(b[1])<<8, + Top: b[2], + Bottom: b[3], + Left: b[4], + Width: b[5], + } + } + return sf, nil +} + +func mustAtoi(s string) int { v, _ := strconv.Atoi(s); return v } + +// loadCompressed forwards each compressed block to the draw device verbatim. +func (i *Image) loadCompressed(r io.Reader, ir Rectangle) error { + var bh [2 * 12]byte + miny := ir.Min.Y + for miny != ir.Max.Y { + if _, err := io.ReadFull(r, bh[:]); err != nil { + return fmt.Errorf("short block header: %w", err) + } + maxy := int32(mustAtoi(trim(string(bh[0:12])))) + nb := mustAtoi(trim(string(bh[12:24]))) + if maxy <= miny || maxy > ir.Max.Y || nb <= 0 { + return fmt.Errorf("bad block: maxy=%d nb=%d", maxy, nb) + } + buf := make([]byte, 21+nb) + buf[0] = 'Y' + put32(buf, 1, i.id) + put32(buf, 5, uint32(ir.Min.X)) + put32(buf, 9, uint32(miny)) + put32(buf, 13, uint32(ir.Max.X)) + put32(buf, 17, uint32(maxy)) + if _, err := io.ReadFull(r, buf[21:]); err != nil { + return fmt.Errorf("short block: %w", err) + } + if err := i.d.write(buf); err != nil { + return err + } + miny = maxy + } + return nil +} + +// loadRaw uploads an uncompressed image a stripe at a time with 'y'. +func (i *Image) loadRaw(r io.Reader, ir Rectangle, chn uint32) error { + depth := chanDepth(chn) + if depth == 0 { + return fmt.Errorf("bad channel %#x", chn) + } + bpl := bytesPerLine(ir, depth) + // Keep each write comfortably inside the device's iounit. + rows := 8000 / bpl + if rows < 1 { + rows = 1 + } + for y := ir.Min.Y; y < ir.Max.Y; { + y1 := y + int32(rows) + if y1 > ir.Max.Y { + y1 = ir.Max.Y + } + n := bpl * int(y1-y) + buf := make([]byte, 21+n) + buf[0] = 'y' + put32(buf, 1, i.id) + put32(buf, 5, uint32(ir.Min.X)) + put32(buf, 9, uint32(y)) + put32(buf, 13, uint32(ir.Max.X)) + put32(buf, 17, uint32(y1)) + if _, err := io.ReadFull(r, buf[21:]); err != nil { + return fmt.Errorf("short pixel data: %w", err) + } + if err := i.d.write(buf); err != nil { + return err + } + y = y1 + } + return nil +} + +// chanDepth is bits per pixel for a channel descriptor. +func chanDepth(c uint32) int { + d := 0 + for ; c != 0; c >>= 8 { + d += int(c & 15) + } + return d +} + +// bytesPerLine mirrors libdraw's unitsperline(r, d, 8), including the +// negative-min.x case. +func bytesPerLine(r Rectangle, d int) int { + if d <= 0 || d > 32 { + return 0 + } + if r.Min.X >= 0 { + l := (int(r.Max.X)*d + 7) / 8 + return l - (int(r.Min.X)*d)/8 + } + l := (int(r.Max.X)*d + 7) / 8 + return l + (int(-r.Min.X)*d+7)/8 +} diff --git a/gui/draw/keyboard.go b/gui/draw/keyboard.go new file mode 100644 index 0000000..3885364 --- /dev/null +++ b/gui/draw/keyboard.go @@ -0,0 +1,65 @@ +package draw + +import ( + "bufio" + "os" + "unicode/utf8" +) + +// Keyboardctl delivers runes typed at the window. +// +// Opening it also turns the console raw. Without that, rio keeps its own +// line editor on the window and echoes what you type into the text layer, +// painting over whatever the program has drawn. Holding consctl open is +// what keeps raw mode; closing it puts the window back as it was. +type Keyboardctl struct { + C <-chan rune + ctl *os.File + cons *os.File +} + +func OpenKeyboard(dev string) (*Keyboardctl, error) { + ctl, err := os.OpenFile(dev+"/consctl", os.O_WRONLY, 0) + if err != nil { + return nil, err + } + if _, err := ctl.WriteString("rawon"); err != nil { + ctl.Close() + return nil, err + } + cons, err := os.Open(dev + "/cons") + if err != nil { + ctl.Close() + return nil, err + } + + ch := make(chan rune, 32) + k := &Keyboardctl{C: ch, ctl: ctl, cons: cons} + go func() { + defer close(ch) + br := bufio.NewReader(cons) + var buf []byte + b := make([]byte, 1) + for { + n, err := br.Read(b) + if n == 0 || err != nil { + return + } + buf = append(buf, b[0]) + // a rune may arrive a byte at a time + if r, sz := utf8.DecodeRune(buf); r != utf8.RuneError || sz > 1 { + ch <- r + buf = buf[:0] + } else if len(buf) >= utf8.UTFMax { + buf = buf[:0] + } + } + }() + return k, nil +} + +func (k *Keyboardctl) Close() error { + k.cons.Close() + // dropping consctl is what restores rio's own line editing + return k.ctl.Close() +} diff --git a/gui/draw/mouse.go b/gui/draw/mouse.go new file mode 100644 index 0000000..a4ddf63 --- /dev/null +++ b/gui/draw/mouse.go @@ -0,0 +1,74 @@ +package draw + +import ( + "fmt" + "os" + "strconv" +) + +// Mouse is one record from /dev/mouse. +type Mouse struct { + Point + Buttons int + Msec uint32 +} + +// Mousectl delivers mouse and resize events on channels. This is the shape +// libdraw's Mousectl already has -- it is hand-rolled CSP in C because there +// were no goroutines. Here it is what the language does natively. +type Mousectl struct { + C <-chan Mouse + Resize <-chan struct{} + + f *os.File +} + +func OpenMouse(dev string) (*Mousectl, error) { + if dev == "" { + dev = "/dev" + } + f, err := os.OpenFile(dev+"/mouse", os.O_RDWR, 0) + if err != nil { + return nil, fmt.Errorf("open mouse: %w", err) + } + c := make(chan Mouse) + rc := make(chan struct{}, 1) + mc := &Mousectl{C: c, Resize: rc, f: f} + // One goroutine parked in read(2). Plan 9 has no netpoll in the Go + // runtime, so this pins an OS thread for as long as it blocks -- fine + // for the two or three event sources a UI has, not for hundreds. + go func() { + defer close(c) + buf := make([]byte, 1+5*12) + for { + n, err := f.Read(buf) + if err != nil { + return + } + if n != 1+4*12 { + continue + } + fld := func(i int) int { + v, _ := strconv.Atoi(trim(string(buf[1+i*12 : 1+i*12+12]))) + return v + } + switch buf[0] { + case 'r': + select { + case rc <- struct{}{}: + default: + } + fallthrough + case 'm': + c <- Mouse{ + Point: Point{int32(fld(0)), int32(fld(1))}, + Buttons: fld(2), + Msec: uint32(fld(3)), + } + } + } + }() + return mc, nil +} + +func (mc *Mousectl) Close() error { return mc.f.Close() } diff --git a/gui/draw/snarf.go b/gui/draw/snarf.go new file mode 100644 index 0000000..ae26edd --- /dev/null +++ b/gui/draw/snarf.go @@ -0,0 +1,15 @@ +package draw + +import "os" + +// Snarf puts text on the system snarf buffer, which is what every other +// program on the system means by "copy". +func Snarf(dev, text string) error { + f, err := os.OpenFile(dev+"/snarf", os.O_WRONLY|os.O_TRUNC, 0) + if err != nil { + return err + } + defer f.Close() + _, err = f.WriteString(text) + return err +} diff --git a/gui/draw/window.go b/gui/draw/window.go new file mode 100644 index 0000000..0de8070 --- /dev/null +++ b/gui/draw/window.go @@ -0,0 +1,86 @@ +package draw + +import ( + "fmt" + "os" + "strconv" + "strings" +) + +// NamedImage looks up an image the window system published by name and +// installs it under a fresh id of ours. The 'n' message also sets the +// client's infoid, so the image's geometry comes back from a plain read of +// the ctl file -- the same 12-field block that draw/new returns. +func (d *Display) NamedImage(name string) (*Image, error) { + if name == "" || len(name) >= 256 { + return nil, fmt.Errorf("namedimage: bad name %q", name) + } + d.mu.Lock() + d.nextid++ + id := d.nextid + d.mu.Unlock() + + b := make([]byte, 6+len(name)) + b[0] = 'n' + put32(b, 1, id) + b[5] = byte(len(name)) + copy(b[6:], name) + if err := d.write(b); err != nil { + return nil, fmt.Errorf("namedimage %q: %w", name, err) + } + + var info [12 * 12]byte + n, err := d.ctl.ReadAt(info[:], 0) + if err != nil && n < len(info) { + return nil, fmt.Errorf("namedimage %q: ctl read: %w", name, err) + } + fld := func(i int) int32 { + v, _ := strconv.Atoi(trim(string(info[i*12 : i*12+12]))) + return int32(v) + } + return &Image{ + d: d, + id: id, + chn: strToChan(trim(string(info[2*12 : 3*12]))), + repl: fld(3) != 0, + R: Rect(fld(4), fld(5), fld(6), fld(7)), + Clipr: Rect(fld(8), fld(9), fld(10), fld(11)), + }, nil +} + +// Window returns the image to draw into: the rio window named by +// <dev>/winname if there is one, otherwise the raw screen. +// +// Without this a program draws to image 0 -- the screen itself -- straight +// over the window system. Nothing stops it: /dev/draw is the kernel's +// device, not rio's, and rio publishes the window by name rather than +// proxying the device. Being a good citizen is opt-in, and this is the +// opt-in. +func (d *Display) Window(dev string) (*Image, error) { + if dev == "" { + dev = "/dev" + } + buf, err := os.ReadFile(dev + "/winname") + if err != nil { + return d.Screen, nil // not under a window system + } + name := strings.TrimRight(string(buf), "\x00\n ") + if name == "" { + return d.Screen, nil + } + win, err := d.NamedImage(name) + if err != nil { + return nil, err + } + return win, nil +} + +// Reattach re-reads winname and looks the window up again. rio replaces the +// window's image on resize rather than resizing it in place, so a resize +// event means "throw the old one away and ask again". +func (d *Display) Reattach(dev string, old *Image) (*Image, error) { + if old != nil && old.id != 0 { + old.Free() // ignore errors: the old image may already be gone + } + return d.Window(dev) +} diff --git a/gui/go.mod b/gui/go.mod new file mode 100644 index 0000000..e5c497d --- /dev/null +++ b/gui/go.mod @@ -0,0 +1,3 @@ +module 9front/gui + +go 1.23 diff --git a/gui/probe/go.mod b/gui/probe/go.mod new file mode 100644 index 0000000..6edd0a9 --- /dev/null +++ b/gui/probe/go.mod @@ -0,0 +1,3 @@ +module probe + +go 1.21 diff --git a/gui/probe/main.go b/gui/probe/main.go new file mode 100644 index 0000000..26b925b --- /dev/null +++ b/gui/probe/main.go @@ -0,0 +1,5 @@ +package main + +import "fmt" + +func main() { fmt.Println("hello from go on plan9") } |
