passgo/cmd/passgo-gui/main.go

884 lines
20 KiB
Go

// +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"))
initPgp(w)
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()
}
idBtns := make([]*Button, 0)
updateIdBtns := func() {
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), noidLabelText)
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:
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()
}
}
updateBtn := &Button{
Size: unit.Sp(fontSize),
Label: "Update",
Alignment: text.Middle,
Color: black,
Background: gray,
}
idEd := &widget.Editor{SingleLine: true, Submit: true}
idSubmitBtn := &Button{
Size: unit.Sp(fontSize),
Label: "Submit",
Alignment: text.Middle,
Color: black,
Background: gray,
}
idPage = func() {
if !animating {
animOn()
}
c2 := flex.Rigid(gtx, func() {
idLabel.Layout(gtx)
})
var c3 layout.FlexChild
var c4 layout.FlexChild
var c5 layout.FlexChild
var c6 layout.FlexChild
var c7 layout.FlexChild
if len(idBtns) == 0 {
c3 = flex.Rigid(gtx, func() {
updateBtn.Layout(gtx)
})
if updateBtn.Clicked() {
updateIdBtns()
w.Invalidate()
}
c4 = flex.Rigid(gtx, func() {
th.Editor("id").Layout(gtx, idEd)
})
for _, e := range idEd.Events(gtx) {
switch e.(type) {
case widget.SubmitEvent:
log(Info, "Submit")
store.Id = idEd.Text()
page = listPage
}
}
c5 = flex.Rigid(gtx, func() {
idSubmitBtn.Layout(gtx)
})
if idSubmitBtn.Clicked() {
store.Id = idEd.Text()
page = listPage
}
c6 = flex.Rigid(gtx, func() {
noidLabel.Layout(gtx)
})
} else {
c3 = flex.Rigid(gtx, func() { })
c4 = flex.Rigid(gtx, func() { })
c5 = flex.Rigid(gtx, func() { })
c6 = flex.Rigid(gtx, func() { })
}
c7 = flex.Rigid(gtx, func() {
if len(idBtns) > 0 { // still zero after update
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, c4, c5, c6, c7)
}
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 func() {
err := store.Insert(passnameEd.Text(), passvalEd.Text())
if err != nil {
page = idPage
}
}()
}
}
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 func() {
err := store.Insert(insName, insValue)
if err != nil {
page = idPage
}
}()
}
}
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 {
go func() {
updateIdBtns()
}()
}
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)
}
}
}
}