// +build darwin linux package main import ( "fmt" "io/ioutil" "os" "path" "runtime" "runtime/pprof" "strconv" "strings" "sync" "time" "gioui.org/app" "gioui.org/io/key" "gioui.org/io/system" "gioui.org/layout" "gioui.org/text" "gioui.org/unit" "gioui.org/widget" "gioui.org/widget/material" "gioui.org/font/gofont" "github.com/fsnotify/fsnotify" "gopkg.in/yaml.v2" "git.wow.st/gmp/passgo" "git.wow.st/gmp/rand" ) type conf struct { StoreDir string ClearDelay int } func main() { if false { go func() { f, err := os.Create("cpuprofile") if err != nil { fmt.Printf("Can't create CPU profile\n") os.Exit(-1) } fmt.Printf("Starting CPU profile\n") if err := pprof.StartCPUProfile(f); err != nil { fmt.Printf("Can't start CPU profile\n") f.Close() os.Exit(-1) } time.Sleep(time.Second * 10) fmt.Printf("Stopping CPU profile\n") pprof.StopCPUProfile() f.Close() fmt.Printf("CPU profile written\n") }() } var fd *os.File var err error confDir, err = getConfDir() if err != nil { fmt.Printf("Can't get config directory") os.Exit(-1) } 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) go func() { err = passgo.GetStore(&store) if err != nil { log(Info, 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{}) chdir = make(chan struct{}) passch = make(chan []byte) go func() { log(Info,"passgo.Identities()") passgo.Identities() }() go Updater() log(Info, "Staring event loop") go eventLoop() app.Main() log(Info, "Event loop returned") } var ( fontSize float32 confDir string Config conf l []passgo.Pass mux sync.Mutex store passgo.Store reload chan struct{} updated chan struct{} chdir chan struct{} passch chan []byte th *material.Theme ) func Updater() { time.Sleep(time.Second * 2) update := func() { fmt.Printf("update()\n") 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) } dir := store.Dir watcher.Add(dir) for { select { case <-reload: update() case <-watcher.Events: update() case e := <-watcher.Errors: log(Info, "Watcher error: ", e) case <-chdir: err := watcher.Remove(dir) if err != nil { log(Info, "Error removing watcher: ", err) } watcher.Add(store.Dir) update() } } } 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(Error, "Config file = ", path.Join(confDir, "config.yml")) log(Fatal, "Cannot open config file: ", err.Error()) } } 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() { gofont.Register() th = material.NewTheme() th.TextSize = unit.Sp(fontSize) w := app.NewWindow( app.Size(unit.Dp(250), unit.Dp(500)), app.Title("passgo")) gtx := &layout.Context{Queue: w.Queue()} time.Sleep(time.Second/5) var margincs layout.Constraints var c1 layout.FlexChild // flex child for title bar sysinset := &layout.Inset{} margin := layout.UniformInset(unit.Dp(10)) title := th.Body1("passgo") dotsBtn := &Button{ Size: unit.Sp(fontSize), Label: "\xe2\x8b\xae", Alignment: text.Middle, 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{Size: unit.Sp(fontSize), Text: "copied to clipboard", Color: black, Background: darkgray, Alignment: text.Middle, } cleared := &Overlay{Size: unit.Sp(fontSize), 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{ Size: unit.Sp(fontSize), Label: strings.Join([]string{s, n, z}, ""), Background: gray, }) pathnames = append(pathnames, x.Pathname) } mux.Unlock() } updateBtns() idBtns := make([]*Button, 0) updateIdBtns := func() { //return log(Info,"passgo.Identities()") ids, err := passgo.Identities() if err != nil { log(Info, err) return } for i, n := range ids { if i >= len(idBtns) { idBtns = append(idBtns, &Button{ Size: unit.Sp(fontSize), Label: n, Alignment: text.End, Color: black, Background: gray, }) } else { idBtns[i].Label = n } } idBtns = idBtns[:len(ids)] } confBtn := &Button{ Size: unit.Sp(fontSize), Label: "configure", Alignment: text.Middle, Color: black, Background: gray, } storeDirLabel := th.Label(unit.Sp(fontSize), "Store directory") storeDirEd := &widget.Editor{ SingleLine: true} storeDirEd.SetText(store.Dir) saveBtn := &Button{ Size: unit.Sp(fontSize), Label: "save", Alignment: text.End, Color: black, Background: gray, } backBtn := &Button{ Size: unit.Sp(fontSize), Label: "back", Alignment: text.End, Color: black, Background: gray, } confirmLabel := th.Label(unit.Sp(fontSize), "Password exists. Overwrite?") yesBtn := &Button{ Size: unit.Sp(fontSize), Label: "yes", Alignment: text.End, Color: black, Background: gray, } promptLabel := th.Label(unit.Sp(fontSize), "passphrase") promptEd := &widget.Editor{ SingleLine: true, Submit: true } okBtn := &Button{ Size: unit.Sp(fontSize), Label: "ok", Alignment: text.End, Color: black, Background: gray, } plusBtn := &Button{ Size: unit.Sp(fontSize), Label: "+", Alignment: text.Middle, Color: black, Background: gray, } insertLabel := th.Label(unit.Sp(fontSize), "Insert") passnameLabel := th.Label(unit.Sp(fontSize), "password name:") passnameEd := &widget.Editor{ SingleLine: true } passvalLabel := th.Label(unit.Sp(fontSize), "password value:") passvalEd := &widget.Editor{ SingleLine: true, Submit: true } noidLabel := th.Label(unit.Sp(fontSize), "No GPG ids available. Please create a private key") idLabel := th.Label(unit.Sp(fontSize), "Select ID") anim := &time.Ticker{} animating := false animOn := func() { log(Info, "animOn()") anim = time.NewTicker(time.Second / 30) animating = true w.Invalidate() } animOff := func() { log(Info, "animOff()") anim.Stop() animating = false } var listPage, idPage, insertPage, confirmPage, confPage, promptPage, page func() _ = idPage 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 c2 := flex.Flex(gtx, 1.0, func() { mux.Lock() if lst.Dragging() { key.HideInputOp{}.Add(gtx.Ops) } switch { case store.Empty: confBtn.Layout(gtx) if confBtn.Clicked() { log(Info, "Configure") w.Invalidate() page = confPage } //case store.Id == "": // idLabel.Layout(gtx) // w.Invalidate() // //page = idPage default: gtx.Constraints.Width.Min = gtx.Constraints.Width.Max lst.Layout(gtx, len(passBtns), func(i int) { btn := passBtns[i] gtx.Constraints.Width.Min = gtx.Constraints.Width.Max btn.Layout(gtx) 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.Millisecond * time.Duration(fade1a*1000)) animOn() }() go func() { time.Sleep(time.Millisecond * time.Duration(Config.ClearDelay*1000)) log(Info, "clearing clipboard") passgo.Clip("") }() } else { log(Info, "Can't decrypt ", name) log(Info, err) } }(pathnames[i]) } }) } mux.Unlock() }) flex.Layout(gtx, 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 } gtx.Constraints = margincs al := layout.Align(layout.SE) al.Layout(gtx, func() { gtx.Constraints.Width.Min = gtx.Constraints.Width.Max overlay.Layout(gtx) }) } if x > start2 && x < fade2a { // animOff() } if animating && x > end { animOff() } } idPage = func() { log(Info,"idPage()") if !animating { animOn() } c2 := flex.Rigid(gtx, func() { idLabel.Layout(gtx) }) //if len(idBtns) == 0 { updateIdBtns() //} c3 := flex.Rigid(gtx, func() { if len(idBtns) == 0 { // still zero after update noidLabel.Layout(gtx) } else { for i := 0; i < len(idBtns); i++ { lst.Layout(gtx, len(idBtns), func(i int) { idBtns[i].Layout(gtx) }) } for _, btn := range idBtns { if btn.Clicked() { log(Info, "ID selected: ", btn.Label) store.SetID(btn.Label) w.Invalidate() animOff() page = listPage } } } }) flex.Layout(gtx, c1, c2, c3) } var insName, insValue string genBtn := &SelButton{SelColor: gray} genBtn.Button = Button{Size: unit.Sp(fontSize), Label: "generate"} symBtn := &SelButton{SelColor: gray} numBtn := &SelButton{SelColor: gray} symBtn.Button = Button{Size: unit.Sp(fontSize), Label: "@"} numBtn.Button = Button{Size: unit.Sp(fontSize), Label: "#"} symBtn.Select() numBtn.Select() lenEd := &widget.Editor{ SingleLine: true, Alignment: text.End } lenEd.SetText("15") lBtn := &Button{Size: unit.Sp(fontSize), Label: "<", Background: gray} rBtn := &Button{Size: unit.Sp(fontSize), Label: ">", Background: gray} updatePw := func() { if !genBtn.Selected { passvalEd.SetText("") return } var gen rand.Generator switch { case symBtn.Selected && numBtn.Selected: gen = rand.Char case symBtn.Selected: gen = rand.LettersSymbols case numBtn.Selected: gen = rand.LetterDigits default: gen = rand.Letter } l, _ := strconv.Atoi(lenEd.Text()) pw, _ := rand.Slice(gen, l) passvalEd.SetText(string(pw)) } insertPage = func() { c2 := flex.Rigid(gtx, func() { insertLabel.Layout(gtx) }) c3 := flex.Rigid(gtx, func() { passnameLabel.Layout(gtx) }) c4 := flex.Rigid(gtx, func() { th.Editor("name").Layout(gtx, passnameEd) }) c5 := flex.Rigid(gtx, func() { passvalLabel.Layout(gtx) }) c6 := flex.Rigid(gtx, func() { th.Editor("password").Layout(gtx, passvalEd) }) btnflx := &layout.Flex{Axis: layout.Horizontal} al := layout.Align(layout.E) c7 := flex.Rigid(gtx, func() { bc1 := btnflx.Rigid(gtx, func() { lBtn.Layout(gtx) }) bc2 := btnflx.Rigid(gtx, func() { gtx.Constraints.Width.Min = 60 th.Editor("len").Layout(gtx, lenEd) }) bc3 := btnflx.Rigid(gtx, func() { rBtn.Layout(gtx) }) bc4 := btnflx.Rigid(gtx, func() { symBtn.Layout(gtx) }) bc5 := btnflx.Rigid(gtx, func() { numBtn.Layout(gtx) }) bc6 := btnflx.Rigid(gtx, func() { genBtn.Layout(gtx) }) al.Layout(gtx, func() { btnflx.Layout(gtx, bc1, bc2, bc3, bc4, bc5, bc6) }) }) c8 := flex.Rigid(gtx, func() { bc1 := btnflx.Rigid(gtx, func() { backBtn.Layout(gtx) }) bc2 := btnflx.Rigid(gtx, func() { saveBtn.Layout(gtx) }) al.Layout(gtx, func() { btnflx.Layout(gtx, bc1, bc2) }) }) flex.Layout(gtx, c1, c2, c3, c4, c5, c6, c7, c8) if lBtn.Clicked() { l, _ := strconv.Atoi(lenEd.Text()) if l > 0 { l -= 1 } lenEd.SetText(strconv.Itoa(l)) updatePw() w.Invalidate() } if rBtn.Clicked() { l, _ := strconv.Atoi(lenEd.Text()) lenEd.SetText(strconv.Itoa(l + 1)) updatePw() w.Invalidate() } if genBtn.Clicked() { updatePw() w.Invalidate() } if symBtn.Clicked() { updatePw() w.Invalidate() } if numBtn.Clicked() { updatePw() w.Invalidate() } if backBtn.Clicked() { w.Invalidate() page = listPage } if saveBtn.Clicked() { w.Invalidate() page = listPage insName = passnameEd.Text() insValue = passvalEd.Text() for _, n := range pathnames { if insName == n { log(Info, "Password exists") page = confirmPage w.Invalidate() return } } //Do not block the UI thread. go store.Insert(passnameEd.Text(), passvalEd.Text()) } } confirmPage = func() { c2 := flex.Rigid(gtx, func() { confirmLabel.Layout(gtx) }) al := layout.Align(layout.E) btnflx := &layout.Flex{Axis: layout.Horizontal} c3 := flex.Rigid(gtx, func() { bc1 := btnflx.Rigid(gtx, func() { backBtn.Layout(gtx) }) bc2 := btnflx.Rigid(gtx, func() { yesBtn.Layout(gtx) }) al.Layout(gtx, func() { btnflx.Layout(gtx, bc1, bc2) }) }) flex.Layout(gtx, c1, c2, c3) if backBtn.Clicked() { w.Invalidate() page = insertPage } if yesBtn.Clicked() { w.Invalidate() page = listPage go store.Insert(insName, insValue) } } confPage = func() { c2 := flex.Rigid(gtx, func() { storeDirLabel.Layout(gtx) }) c3 := flex.Rigid(gtx, func() { th.Editor("directory").Layout(gtx, storeDirEd) }) al := layout.Align(layout.E) c4 := flex.Rigid(gtx, func() { btnflx := &layout.Flex{Axis: layout.Horizontal} bc1 := btnflx.Rigid(gtx, func() { backBtn.Layout(gtx) }) bc2 := btnflx.Rigid(gtx, func() { saveBtn.Layout(gtx) }) al.Layout(gtx, func() { btnflx.Layout(gtx, bc1, bc2) }) }) flex.Layout(gtx, 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() store.Id = "" passgo.GetStore(&store) Config.StoreDir = store.Dir store.Mkdir() saveConf() chdir <- struct{}{} }() w.Invalidate() page = listPage } } promptPage = func() { submit := false for _, e := range promptEd.Events(gtx) { switch e.(type) { case widget.SubmitEvent: log(Info, "Submit") submit = true } } c2 := flex.Rigid(gtx, func() { promptLabel.Layout(gtx) }) c3 := flex.Rigid(gtx, func() { th.Editor("password").Layout(gtx, promptEd) }) c4 := flex.Rigid(gtx, func() { al := layout.Align(layout.E) btnflx := &layout.Flex{Axis: layout.Horizontal} bc1 := btnflx.Rigid(gtx, func() { backBtn.Layout(gtx) }) bc2 := btnflx.Rigid(gtx, func() { okBtn.Layout(gtx) }) al.Layout(gtx, func() { btnflx.Layout(gtx, bc1, bc2) }) }) flex.Layout(gtx, 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 ms := &runtime.MemStats{} x := 0 var mallocs uint64 for { select { case <-updated: log(Info, "UPDATE") updateBtns() w.Invalidate() case <-anim.C: w.Invalidate() case e := <-w.Events(): x++ if x == 100 { runtime.ReadMemStats(ms) mallocs = ms.Mallocs } switch e := e.(type) { case system.DestroyEvent: return case system.StageEvent: if e.Stage == system.StageRunning { initPgp(w) } case system.FrameEvent: gtx.Reset(e.Config, e.Size) sysinset.Top = e.Insets.Top sysinset.Bottom = e.Insets.Bottom sysinset.Left = e.Insets.Left sysinset.Right = e.Insets.Right sysinset.Layout(gtx, func() { margin.Layout(gtx, func() { margincs = gtx.Constraints c1 = flex.Rigid(gtx, func() { ct2 := titleflex.Rigid(gtx, func() { plusBtn.Layout(gtx) }) ct3 := titleflex.Rigid(gtx, func() { dotsBtn.Layout(gtx) }) ct1 := titleflex.Flex(gtx, 1.0, func() { gtx.Constraints.Width.Min = gtx.Constraints.Width.Max title.Layout(gtx) }) titleflex.Layout(gtx, ct1, ct2, ct3) }) page() }) }) if dotsBtn.Clicked() { log(Info, "Configure") w.Invalidate() page = confPage } if plusBtn.Clicked() { log(Info, "Plus") w.Invalidate() insName, insValue = "", "" page = insertPage } e.Frame(gtx.Ops) } if x == 100 { x = 0 runtime.ReadMemStats(ms) fmt.Printf("mallocs: %d\n", ms.Mallocs-mallocs) } } } }