// +build darwin linux package main import ( "image" "image/color" "io/ioutil" "os" "path" "strings" "sync" "time" "gioui.org/ui" "gioui.org/ui/app" "gioui.org/ui/f32" "gioui.org/ui/gesture" "gioui.org/ui/input" "gioui.org/ui/key" "gioui.org/ui/layout" "gioui.org/ui/measure" "gioui.org/ui/paint" "gioui.org/ui/pointer" "gioui.org/ui/text" "github.com/fsnotify/fsnotify" "gopkg.in/yaml.v2" "git.wow.st/gmp/passgo" ) type conf struct { StoreDir string } func main() { var fd *os.File confFile := path.Join(confDir, "config.yml") if _, err := os.Stat(confFile); os.IsNotExist(err) { fd, err = os.Create(confFile) if err != nil { log(Fatal, "Cannot create configuration file: ", err) } } confbytes, err := ioutil.ReadFile(confFile) if err != nil { log(Fatal, "Cannot read configuration file: ", err) } if err = yaml.UnmarshalStrict(confbytes, &Config); err != nil { log(Fatal, "Cannot parse configuration file: ", err) } store.Dir = Config.StoreDir log(Info, " StoreDir = ", store.Dir) err = passgo.GetStore(&store) if err != nil { log(Fatal, err) } if fd != nil { // we still have an empty conf file open: Config.StoreDir = store.Dir saveConf(fd) } reload = make(chan struct{}) updated = make(chan struct{}) go Updater() log(Info, "Staring event loop") go eventLoop() app.Main() log(Info, "Event loop returned") } var ( Config conf l []passgo.Pass mux sync.Mutex store passgo.Store reload chan struct{} updated chan struct{} black = color.RGBA{A: 0xff, R: 0, G: 0, B: 0} white = color.RGBA{A: 0xff, R: 0xff, G: 0xff, B: 0xff} gray = color.RGBA{A: 0xff, R: 0xf0, G: 0xf0, B: 0xf0} darkgray = color.RGBA{A: 0xff, R: 0xa0, G: 0xa0, B: 0xa0} ) func Updater() { update := func() { ltmp, err := store.List() if err != nil { log(Info, 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 <-reload: update() case <-watcher.Events: update() case e := <-watcher.Errors: log(Info, "Watcher error: ", e) } } } func saveConf(fds ...*os.File) { var fd *os.File var err error if len(fds) > 0 && fds[0] != nil { fd = fds[0] } else { fd, err = os.Create(path.Join(confDir, "config.yml")) if err != nil { log(Fatal, "Cannot open config file: ", err) } } confbytes, err := yaml.Marshal(Config) if err != nil { log(Fatal, "Cannot save configuration: ", err) } _, err = fd.Write(confbytes) if err != nil { log(Fatal, "Cannot write to configuration: ", err) } } type Overlay struct { Face text.Face Text string Click gesture.Click Color color.RGBA Background color.RGBA Alignment text.Alignment } func (b *Overlay) Layout(c ui.Config, ops *ui.Ops, cs layout.Constraints) layout.Dimensions { 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.Text, Alignment: b.Alignment, } ins := layout.UniformInset(ui.Dp(4)) l.Material.Record(ops) paint.ColorOp{Color: b.Color}.Add(ops) l.Material.Stop() 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) } c2 := st.End(dims) c1 := st.End(layoutRRect(b.Background, c, ops, st.Expand())) dims = st.Layout(c1, c2) return ins.End(dims) } type Button struct { Face text.Face Label string Click gesture.Click Color color.RGBA Background color.RGBA Alignment text.Alignment 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, q input.Queue, ops *ui.Ops, 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, Alignment: b.Alignment, } ins := layout.UniformInset(ui.Dp(4)) paint.ColorOp{Color: b.Color}.Add(ops) 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.Background, 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() var c ui.Config ops := new(ui.Ops) var dims layout.Dimensions var cs layout.Constraints var margincs layout.Constraints var faces measure.Faces var c1 layout.FlexChild // flex child for title bar face := faces.For(regular, ui.Sp(16)) margin := layout.UniformInset(ui.Dp(10)) title := &text.Label{Face: face, Text: "passgo"} dotsbtn := &Button{ Face: face, Label: "\xe2\x8b\xae", Alignment: text.End, Color: black, Background: gray, } titleflex := &layout.Flex{Axis: layout.Horizontal} flex := &layout.Flex{Axis: layout.Vertical} passInput := &text.Editor{Face: face, SingleLine: true} passInput.SetText("passphrase") passSubmit := &Button{Face: face, Label: "submit", Color: black} lst := &layout.List{Axis: layout.Vertical} passBtns := make([]*Button, 0) _ = passInput _ = passSubmit pathnames := make([]string, 0) copied := &Overlay{Face: face, Text: "copied to clipboard", Color: black, Background: darkgray, Alignment: text.Middle, } var copiedWhen time.Time 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}, ""), Background: gray, }) pathnames = append(pathnames, x.Pathname) } mux.Unlock() } updateBtns() confBtn := &Button{ Face: face, Label: "configure", Alignment: text.Middle, Color: black, Background: gray, } storeDirLabel := &text.Label{Face: face, Text: "Store directory"} storeDirEd := &text.Editor{Face: face, SingleLine: true} storeDirEd.SetText(store.Dir) saveBtn := &Button{ Face: face, Label: "save", Alignment: text.End, Color: black, Background: gray, } backBtn := &Button{ Face: face, Label: "back", Alignment: text.End, Color: black, Background: gray, } anim := &time.Ticker{} animating := false animOn := func() { anim = time.NewTicker(time.Second / 120) animating = true w.Invalidate() } animOff := func() { anim.Stop() animating = false } var listPage, confPage, page func() _ = confPage listPage = func() { cs = flex.Flexible(1.0) mux.Lock() if lst.Dragging() { key.HideInputOp{}.Add(ops) } var c2 layout.FlexChild if len(passBtns) == 0 { c2 = flex.End(confBtn.Layout(c, q, ops, cs)) if confBtn.Clicked() { log(Info, "Configure") w.Invalidate() page = confPage } } else { for lst.Init(c, q, ops, cs, len(passBtns)); lst.More(); lst.Next() { btn := passBtns[lst.Index()] dims = btn.Layout(c, q, ops, lst.Constraints()) lst.End(dims) if btn.Clicked() { log(Info, "Clicked ", btn.Label) // don't block UI thread on decryption attempt go func(name string) { //p,err := store.Decrypt(name, prompt) p, err := store.Decrypt(name) if err == nil { passgo.Clip(p) copiedWhen = time.Now() animOn() } else { log(Info, "Can't decrypt ", name) log(Info, err) } }(pathnames[lst.Index()]) } } c2 = flex.End(lst.Layout()) } mux.Unlock() if dotsbtn.Clicked() { log(Info, "Configure") w.Invalidate() page = confPage } flex.Layout(c1, c2) x := time.Since(copiedWhen).Seconds() start, end := 1.5, 1.75 switch { case x > start && x < end: fade := (end - x) / (end - start) copied.Color.R = uint8(float64(black.R) * fade) copied.Color.G = uint8(float64(black.G) * fade) copied.Color.B = uint8(float64(black.B) * fade) copied.Color.A = uint8(float64(black.A) * fade) copied.Background.R = uint8(float64(darkgray.R) * fade) copied.Background.G = uint8(float64(darkgray.G) * fade) copied.Background.B = uint8(float64(darkgray.B) * fade) copied.Background.A = uint8(float64(darkgray.A) * fade) fallthrough case x <= start: cs = margincs al := layout.Align{Alignment: layout.SE} cs = al.Begin(ops, cs) cs.Width.Min = cs.Width.Max dims = al.End(copied.Layout(c, ops, cs)) case animating: copied.Color = black copied.Background = darkgray animOff() } } confPage = func() { cs = flex.Rigid() c2 := flex.End(storeDirLabel.Layout(ops, cs)) cs = flex.Rigid() c3 := flex.End(storeDirEd.Layout(c, q, ops, cs)) cs = flex.Rigid() al := &layout.Align{Alignment: layout.E} cs = al.Begin(ops, cs) btnflx := &layout.Flex{Axis: layout.Horizontal} btnflx.Init(ops, cs) cs = btnflx.Rigid() bc1 := btnflx.End(backBtn.Layout(c, q, ops, cs)) cs = btnflx.Rigid() bc2 := btnflx.End(saveBtn.Layout(c, q, ops, cs)) dims = btnflx.Layout(bc1, bc2) c4 := flex.End(al.End(dims)) flex.Layout(c1, c2, c3, c4) if backBtn.Clicked() { log(Info, "Back") storeDirEd.SetText(store.Dir) w.Invalidate() page = listPage } if saveBtn.Clicked() { log(Info, "Save") go func() { // do not block UI thread store.Dir = storeDirEd.Text() passgo.GetStore(&store) Config.StoreDir = store.Dir saveConf() reload <- struct{}{} }() w.Invalidate() page = listPage } } page = listPage for { select { case <-updated: log(Info, "UPDATE") updateBtns() w.Invalidate() case <-anim.C: 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) cs = layout.RigidConstraints(e.Size) cs = margin.Begin(c, ops, cs) margincs = cs flex.Init(ops, cs) cs = flex.Rigid() titleflex.Init(ops, cs) cs = titleflex.Rigid() ct2 := titleflex.End(dotsbtn.Layout(c, q, ops, cs)) cs = titleflex.Flexible(1.0) cs.Width.Min = cs.Width.Max ct1 := titleflex.End(title.Layout(ops, cs)) c1 = flex.End(titleflex.Layout(ct1, ct2)) page() margin.End(dims) w.Update(ops) } } } }