// +build darwin linux package main import ( "io/ioutil" "os" "path" "strings" "sync" "time" "gioui.org/ui" "gioui.org/ui/app" "gioui.org/ui/key" "gioui.org/ui/layout" "gioui.org/ui/measure" "gioui.org/ui/text" "github.com/fsnotify/fsnotify" "gopkg.in/yaml.v2" "git.wow.st/gmp/passgo" ) type conf struct { StoreDir string ClearDelay int } 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 Config.ClearDelay == 0 { Config.ClearDelay = 45 } 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{}) passch = make(chan []byte) 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{} passch chan []byte ) 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) } } defer fd.Close() 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) } } 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} lst := &layout.List{Axis: layout.Vertical} passBtns := make([]*Button, 0) pathnames := make([]string, 0) copied := &Overlay{Face: face, Text: "copied to clipboard", Color: black, Background: darkgray, Alignment: text.Middle, } cleared := &Overlay{Face: face, Text: "clipboard cleared", Color: black, Background: darkgray, Alignment: text.Middle, } overlay := copied var overlayStart 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, } promptLabel := &text.Label{Face: face, Text: "passphrase"} promptEd := &text.Editor{Face: face, SingleLine: true, Submit: true} okBtn := &Button{ Face: face, Label: "ok", Alignment: text.End, Color: black, Background: gray, } anim := &time.Ticker{} animating := false animOn := func() { log(Info,"animOn()") anim = time.NewTicker(time.Second / 120) animating = true w.Invalidate() } animOff := func() { log(Info,"animOff()") anim.Stop() animating = false } var listPage, confPage, promptPage, page func() prompt := func() []byte { page = promptPage promptEd.SetText("") w.Invalidate() return <-passch } listPage = func() { // timing variables used for animation fade1a, fade1b := 1.5, 2.0 start2 := float64(Config.ClearDelay) fade2a, end := start2 + 1.5, start2 + 2.0 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() { i := lst.Index() btn := passBtns[i] 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) overlayStart = time.Now() overlay = copied overlay.Color = black overlay.Background = darkgray w.Invalidate() go func() { time.Sleep(time.Second * time.Duration(fade1a)) animOn() }() go func() { time.Sleep(time.Second * time.Duration(Config.ClearDelay)) log(Info, "clearing clipboard") passgo.Clip("") }() } else { log(Info, "Can't decrypt ", name) log(Info, err) } }(pathnames[i]) } } c2 = flex.End(lst.Layout()) } mux.Unlock() if dotsbtn.Clicked() { log(Info, "Configure") w.Invalidate() page = confPage } flex.Layout(c1, c2) x := time.Since(overlayStart).Seconds() if x >= fade1b && x < start2 && animating { animOff() go func() { time.Sleep(time.Millisecond * time.Duration((start2 - x)*1000)) w.Invalidate() time.Sleep(time.Millisecond * time.Duration((fade2a - start2)*1000)) animOn() }() } if (x >= fade1a && x < fade1b) || (x > fade2a && x < end) { if !animating { animOn() } var fade float64 switch { case x < fade1b: fade = (fade1b - x) / (fade1b - fade1a) case x > fade2a: fade = (end - x) / (end - fade2a) } overlay.Color.R = uint8(float64(black.R) * fade) overlay.Color.G = uint8(float64(black.G) * fade) overlay.Color.B = uint8(float64(black.B) * fade) overlay.Color.A = uint8(float64(black.A) * fade) overlay.Background.R = uint8(float64(darkgray.R) * fade) overlay.Background.G = uint8(float64(darkgray.G) * fade) overlay.Background.B = uint8(float64(darkgray.B) * fade) overlay.Background.A = uint8(float64(darkgray.A) * fade) } if x <= fade1b || (x > start2 && x < end) { if x > start2 && overlay == copied { overlay = cleared overlay.Color = black overlay.Background = darkgray } cs = margincs al := layout.Align{Alignment: layout.SE} cs = al.Begin(ops, cs) cs.Width.Min = cs.Width.Max dims = al.End(overlay.Layout(c, ops, cs)) } if x > start2 && x < fade2a { // animOff() } if animating && x > end { 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 } } promptPage = func() { submit := false for e, ok := promptEd.Next(c, q); ok; e, ok = promptEd.Next(c, q) { switch e.(type) { case text.SubmitEvent: log(Info, "Submit") submit = true } } cs = flex.Rigid() c2 := flex.End(promptLabel.Layout(ops, cs)) cs = flex.Rigid() c3 := flex.End(promptEd.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(okBtn.Layout(c, q, ops, cs)) dims = btnflx.Layout(bc1, bc2) c4 := flex.End(al.End(dims)) flex.Layout(c1, c2, c3, c4) if submit || okBtn.Clicked() { log(Info, "Ok") go func() { // do not block UI thread passch <-[]byte(promptEd.Text()) }() w.Invalidate() page = listPage } if backBtn.Clicked() { log(Info, "Back") go func() { passch <- nil // cancel prompt }() 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) } } } }