// +build darwin linux package main import ( //"fmt" "path" "strings" "image" "image/color" "sync" "gioui.org/ui" "gioui.org/ui/app" "gioui.org/ui/input" "gioui.org/ui/key" "gioui.org/ui/layout" "gioui.org/ui/text" "gioui.org/ui/measure" "gioui.org/ui/f32" "gioui.org/ui/paint" "gioui.org/ui/gesture" "gioui.org/ui/pointer" "github.com/fsnotify/fsnotify" "golang.org/x/image/font/gofont/goregular" "golang.org/x/image/font/sfnt" "git.wow.st/gmp/gpass" ) func main() { var err error store,err = gpass.GetStore() if err != nil { log(Fatal, err) } updated = make(chan struct{}) go Updater() log(Info,"Staring event loop") go eventLoop() app.Main() log(Info,"Event loop returned") } var ( l []gpass.Pass mux sync.Mutex store *gpass.Store updated chan struct{} ) func Updater() { update := func() { ltmp,err := store.List() if err != nil { log(Fatal, err) } mux.Lock() l = ltmp mux.Unlock() updated <- struct{}{} } update() watcher, err := fsnotify.NewWatcher() if err != nil { log(Fatal, err) } watcher.Add(store.Dir) for { select { case <-watcher.Events: update() case e := <-watcher.Errors: log(Info, "Watcher error: ",e) } } } type Button struct { Face text.Face Label string Click gesture.Click Color color.RGBA clicked bool } func layoutRRect(col color.RGBA, c ui.Config, ops *ui.Ops, cs layout.Constraints) layout.Dimensions { r := float32(c.Px(ui.Dp(4))) sz := image.Point{X: cs.Width.Min, Y: cs.Height.Min} w, h := float32(sz.X), float32(sz.Y) rrect(ops, w, h, r, r, r, r) paint.ColorOp{Color: col}.Add(ops) paint.PaintOp{Rect: f32.Rectangle{Max: f32.Point{X: w, Y: h}}}.Add(ops) return layout.Dimensions{Size: sz} } // https://pomax.github.io/bezierinfo/#circles_cubic. func rrect(ops *ui.Ops, width, height, se, sw, nw, ne float32) { w, h := float32(width), float32(height) const c = 0.55228475 // 4*(sqrt(2)-1)/3 var b paint.PathBuilder b.Init(ops) b.Move(f32.Point{X: w, Y: h - se}) b.Cube(f32.Point{X: 0, Y: se * c}, f32.Point{X: -se + se*c, Y: se}, f32.Point{X: -se, Y: se}) // SE b.Line(f32.Point{X: sw - w + se, Y: 0}) b.Cube(f32.Point{X: -sw * c, Y: 0}, f32.Point{X: -sw, Y: -sw + sw*c}, f32.Point{X: -sw, Y: -sw}) // SW b.Line(f32.Point{X: 0, Y: nw - h + sw}) b.Cube(f32.Point{X: 0, Y: -nw * c}, f32.Point{X: nw - nw*c, Y: -nw}, f32.Point{X: nw, Y: -nw}) // NW b.Line(f32.Point{X: w - ne - nw, Y: 0}) b.Cube(f32.Point{X: ne * c, Y: 0}, f32.Point{X: ne, Y: ne - ne*c}, f32.Point{X: ne, Y: ne}) // NE b.End() } func (b *Button) Layout(c ui.Config, ops *ui.Ops, q input.Queue, cs layout.Constraints) layout.Dimensions { b.clicked = false for ev, ok := b.Click.Next(q); ok; ev, ok = b.Click.Next(q) { if ev.Type == gesture.TypeClick { b.clicked = true } } ins := layout.UniformInset(ui.Dp(1)) cs = ins.Begin(c, ops, cs) var dims layout.Dimensions st := layout.Stack{} st.Init(ops, cs) { cs = st.Rigid() l := text.Label{ Face: b.Face, Text: b.Label, } ins := layout.UniformInset(ui.Dp(4)) dims = ins.End(l.Layout(ops, ins.Begin(c, ops, cs))) pointer.RectAreaOp{image.Rect(0, 0, dims.Size.X, dims.Size.Y)}.Add(ops) b.Click.Add(ops) } c2 := st.End(dims) c1 := st.End(layoutRRect(b.Color, c, ops, st.Expand())) dims = st.Layout(c1, c2) return ins.End(dims) } func (b *Button) Clicked() bool { return b.clicked } func eventLoop() { w := app.NewWindow(app.WithWidth(ui.Dp(250))) q := w.Queue() _ = q ops := new(ui.Ops) var faces measure.Faces regular, err := sfnt.Parse(goregular.TTF) if err != nil { log(Fatal, "Cannot parse font.") } face := faces.For(regular, ui.Sp(16)) margin := layout.UniformInset(ui.Dp(10)) passInput := &text.Editor{Face: face, SingleLine: true} passInput.SetText("passphrase") passSubmit := &Button{Face: face, Label: "submit"} lst := &layout.List{Axis: layout.Vertical} passBtns := make([]*Button,0) _ = passInput _ = passSubmit pathnames := make([]string,0) updateBtns := func() { passBtns = passBtns[:0] pathnames = pathnames[:0] mux.Lock() for _,x := range l { _, n := path.Split(x.Pathname) s := strings.Repeat(" /",x.Level) z := "" if x.Dir { z = "/" } passBtns = append(passBtns,&Button{ Face:face, Label:strings.Join([]string{s,n,z},""), Color: color.RGBA{A: 0xff, R: 0xf0, G: 0xf0, B: 0xf0}, }) pathnames = append(pathnames, x.Pathname) } mux.Unlock() } updateBtns() for { select { case <-updated: log(Info,"UPDATE") updateBtns() w.Invalidate() case e := <-w.Events(): switch e := e.(type) { case app.DestroyEvent: return case app.UpdateEvent: c := &e.Config ops.Reset() faces.Reset(c) var dims layout.Dimensions cs := layout.RigidConstraints(e.Size) cs = margin.Begin(c, ops, cs) mux.Lock() if lst.Dragging() { key.HideInputOp{}.Add(ops) } for lst.Init(c, q, ops, cs, len(passBtns)); lst.More(); lst.Next() { btn := passBtns[lst.Index()] dims = btn.Layout(c, ops, q, lst.Constraints()) lst.End(dims) if btn.Clicked() { // don't block UI thread on decryption attempt log(Info,"Clicked ", btn.Label) go func(name string) { //p,err := store.Decrypt(name, prompt) p,err := store.Decrypt(name) if err == nil { gpass.Clip(p) } else { log(Info,"Can't decrypt ", name) log(Info,err) } }(pathnames[lst.Index()]) } } mux.Unlock() dims = lst.Layout() dims = margin.End(dims) w.Update(ops) } } } }