From 09411aebdff879141bf58e72cbaf3ed432d02e96 Mon Sep 17 00:00:00 2001 From: flamingcow Date: Sat, 25 Jul 2026 21:41:21 -0700 Subject: [PATCH] Reset everything from a held on-screen button or space --- input.go | 147 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ main.go | 40 ++++++++++----- ui.go | 84 ++++++++++++++++++++++++++++++- 3 files changed, 259 insertions(+), 12 deletions(-) diff --git a/input.go b/input.go index 0a5cf8e..eb421fe 100644 --- a/input.go +++ b/input.go @@ -1,11 +1,158 @@ package main import ( + "fmt" "os" + "path/filepath" + "sync" + "unsafe" "golang.org/x/sys/unix" ) +const ( + evSyn = 0x00 + evKey = 0x01 + evAbs = 0x03 + synReport = 0x00 + btnTouch = 0x14a + absX = 0x00 + absY = 0x01 + + inputPropDirect = 0x01 + sizeofInputEvent = 24 +) + +func evIoc(nr, size uintptr) uintptr { + const iocRead = 2 + return iocRead<<30 | size<<16 | 'E'<<8 | nr +} + +// Current finger position and whether it is down. Held as state rather than +// delivered as events, so a dropped release can never leave a hold armed. +type touchState struct { + mu sync.Mutex + x, y int + down bool +} + +func (t *touchState) set(x, y int, down bool) { + t.mu.Lock() + t.x, t.y, t.down = x, y, down + t.mu.Unlock() +} + +func (t *touchState) get() (x, y int, down bool) { + t.mu.Lock() + defer t.mu.Unlock() + return t.x, t.y, t.down +} + +// Picks the direct-input device that reports absolute X and Y, which is the +// touchscreen; a mouse is relative and a keyboard has no absolute axes. +func findTouchscreen() (*os.File, error) { + paths, err := filepath.Glob("/dev/input/event*") + if err != nil { + return nil, err + } + for _, p := range paths { + f, err := os.OpenFile(p, os.O_RDONLY, 0) + if err != nil { + continue + } + var props, absBits uint64 + if _, _, errno := unix.Syscall(unix.SYS_IOCTL, f.Fd(), + evIoc(0x09, 8), uintptr(unsafe.Pointer(&props))); errno != 0 { + f.Close() + continue + } + if _, _, errno := unix.Syscall(unix.SYS_IOCTL, f.Fd(), + evIoc(0x20+evAbs, 8), uintptr(unsafe.Pointer(&absBits))); errno != 0 { + f.Close() + continue + } + if props&(1<= r.x && x < r.x+r.w && y >= r.y && y < r.y+r.h +} + type display struct { fb *framebuffer huge *textFace grid *textFace gridB *textFace small *textFace + + resetBtn rect + holdStart time.Time + holdFrac float64 + fired bool } func newDisplay() (*display, error) { @@ -75,9 +94,71 @@ func newDisplay() (*display, error) { return nil, fmt.Errorf("grid faces disagree on cell width: %d vs %d", d.grid.cellW, d.gridB.cellW) } + d.resetBtn = rect{ + x: fb.w - uiMargin - btnW, + y: fb.h - uiMargin - btnH, + w: btnW, + h: btnH, + } return d, nil } +// Tracks a press and hold on the reset button, returning true once it has been +// held long enough. Lifting or sliding off cancels, and the press has to be +// released before it can arm again. +func (d *display) holdReset(x, y int, down bool, now time.Time) bool { + if !down { + d.holdStart, d.holdFrac, d.fired = time.Time{}, 0, false + return false + } + if d.fired || !d.resetBtn.contains(x, y) { + d.holdStart, d.holdFrac = time.Time{}, 0 + return false + } + if d.holdStart.IsZero() { + d.holdStart = now + } + d.holdFrac = now.Sub(d.holdStart).Seconds() / holdDuration.Seconds() + if d.holdFrac < 1 { + return false + } + d.holdFrac, d.fired, d.holdStart = 0, true, time.Time{} + return true +} + +func (d *display) drawResetButton() { + r := d.resetBtn + d.fb.rect(r.x, r.y, r.w, r.h, uiButton) + + if d.holdFrac > 0 { + w := int(float64(r.w) * math.Min(d.holdFrac, 1)) + d.fb.rect(r.x, r.y, w, r.h, uiCyan) + } + for _, e := range []rect{ + {r.x, r.y, r.w, 2}, + {r.x, r.y + r.h - 2, r.w, 2}, + {r.x, r.y, 2, r.h}, + {r.x + r.w - 2, r.y, 2, r.h}, + } { + d.fb.rect(e.x, e.y, e.w, e.h, uiCyan) + } + + label := "RESET" + lx := r.x + (r.w-len(label)*d.gridB.cellW)/2 + ly := r.y + (r.h-d.gridB.cellH)/2 + // The label straddles the fill, so each glyph takes the colour that reads + // against whatever is behind it. + split := r.x + int(float64(r.w)*math.Min(d.holdFrac, 1)) + for i, c := range label { + gx := lx + i*d.gridB.cellW + col := uiFg + if gx+d.gridB.cellW/2 < split { + col = uiBg + } + d.gridB.draw(d.fb, gx, ly, string(c), col) + } +} + func (d *display) close() { d.fb.close() } @@ -164,7 +245,7 @@ func (d *display) render(dirs []*direction, views []view, elapsed time.Duration, lineH := d.grid.cellH + 4 sectionH := d.small.cellH + 10 + (1+len(dirs))*lineH gap := 30 - avail := fb.h - (bandY + bandH) - 16 + avail := fb.h - (bandY + bandH) - btnH - 2*uiMargin y := bandY + bandH + 8 + (avail-2*sectionH-gap)/2 y = d.section(y, "RATE") @@ -204,6 +285,7 @@ func (d *display) render(dirs []*direction, views []view, elapsed time.Duration, y += lineH } + d.drawResetButton() fb.flush() }