passgo/cmd/passgo-gui/main.go

666 lines
15 KiB
Go

// +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"
"golang.org/x/image/font/sfnt"
"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(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)
passgo.Identities()
go Updater()
log(Info, "Staring event loop")
go eventLoop()
app.Main()
log(Info, "Event loop returned")
}
var (
confDir string
regular *sfnt.Font
Config conf
l []passgo.Pass
mux sync.Mutex
store passgo.Store
reload chan struct{}
updated chan struct{}
chdir 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)
}
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(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)),
app.WithTitle("passgo"))
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.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{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()
idBtns := make([]*Button, 0)
updateIdBtns := func() {
ids, err := passgo.Identities()
if err != nil {
log(Info, err)
return
}
for i, n := range ids {
if i >= len(idBtns) {
idBtns = append(idBtns, &Button{
Face: face,
Label: n,
Alignment: text.End,
Color: black,
Background: gray,
})
} else {
idBtns[i].Label = n
}
}
idBtns = idBtns[:len(ids)]
}
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,
}
confirmLabel := &text.Label{Face: face, Text: "Password exists. Overwrite?"}
yesBtn := &Button{
Face: face,
Label: "yes",
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,
}
plusBtn := &Button{
Face: face,
Label: "+",
Alignment: text.Middle,
Color: black,
Background: gray,
}
insertLabel := &text.Label{Face: face, Text: "Insert"}
passnameLabel := &text.Label{Face: face, Text: "password name:"}
passnameEd := &text.Editor{Face: face, SingleLine: true, Submit: true}
passvalLabel := &text.Label{Face: face, Text: "password value:"}
passvalEd := &text.Editor{Face: face, SingleLine: true, Submit: true}
noidLabel := &text.Label{Face: face, Text: "No GPG ids available. Please create a private key"}
idLabel := &text.Label{Face: face, Text: "Select ID"}
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, idPage, insertPage, confirmPage, 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
switch {
case store.Empty:
c2 = flex.End(confBtn.Layout(c, q, ops, cs))
if confBtn.Clicked() {
log(Info, "Configure")
w.Invalidate()
page = confPage
}
case store.Id == "":
c2 = flex.End(idLabel.Layout(ops, cs))
w.Invalidate()
page = idPage
default:
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.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])
}
}
c2 = flex.End(lst.Layout())
}
mux.Unlock()
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()
}
}
idPage = func() {
if !animating {
animOn()
}
cs = flex.Rigid()
c2 := flex.End(idLabel.Layout(ops, cs))
var c3 layout.FlexChild
//if len(idBtns) == 0 {
updateIdBtns()
//}
cs = flex.Rigid()
if len(idBtns) == 0 { // still zero after update
c3 = flex.End(noidLabel.Layout(ops, cs))
} else {
for lst.Init(c, q, ops, cs, len(idBtns)); lst.More(); lst.Next() {
lst.End(idBtns[lst.Index()].Layout(c, q, ops, lst.Constraints()))
}
c3 = flex.End(lst.Layout())
for _, btn := range idBtns {
if btn.Clicked() {
log(Info, "ID selected: ", btn.Label)
store.SetID(btn.Label)
w.Invalidate()
animOff()
page = listPage
}
}
}
flex.Layout(c1, c2, c3)
}
var insName, insValue string
insertPage = func() {
cs = flex.Rigid()
c2 := flex.End(insertLabel.Layout(ops, cs))
c3 := flex.End(passnameLabel.Layout(ops, flex.Rigid()))
c4 := flex.End(passnameEd.Layout(c, q, ops, flex.Rigid()))
c5 := flex.End(passvalLabel.Layout(ops, flex.Rigid()))
c6 := flex.End(passvalEd.Layout(c, q, ops, flex.Rigid()))
cs = flex.Rigid()
al := &layout.Align{Alignment: layout.E}
btnflx := &layout.Flex{Axis: layout.Horizontal}
btnflx.Init(ops, al.Begin(ops, cs))
bc1 := btnflx.End(backBtn.Layout(c, q, ops, btnflx.Rigid()))
bc2 := btnflx.End(saveBtn.Layout(c, q, ops, btnflx.Rigid()))
c7 := flex.End(al.End(btnflx.Layout(bc1, bc2)))
flex.Layout(c1, c2, c3, c4, c5, c6, c7)
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
}
}
store.Insert(passnameEd.Text(), passvalEd.Text())
}
}
confirmPage = func() {
cs = flex.Rigid()
c2 := flex.End(confirmLabel.Layout(ops, cs))
al := &layout.Align{Alignment: layout.E}
btnflx := &layout.Flex{Axis: layout.Horizontal}
btnflx.Init(ops, al.Begin(ops, flex.Rigid()))
bc1 := btnflx.End(backBtn.Layout(c, q, ops, btnflx.Rigid()))
bc2 := btnflx.End(yesBtn.Layout(c, q, ops, btnflx.Rigid()))
c3 := flex.End(al.End(btnflx.Layout(bc1, bc2)))
flex.Layout(c1, c2, c3)
if backBtn.Clicked() {
w.Invalidate()
page = insertPage
}
if yesBtn.Clicked() {
w.Invalidate()
page = listPage
store.Insert(insName, insValue)
}
}
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()
store.Id = ""
passgo.GetStore(&store)
Config.StoreDir = store.Dir
store.Mkdir()
saveConf()
chdir <- 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(plusBtn.Layout(c, q, ops, cs))
cs = titleflex.Rigid()
ct3 := 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, ct3))
page()
margin.End(dims)
if dotsBtn.Clicked() {
log(Info, "Configure")
w.Invalidate()
page = confPage
}
if plusBtn.Clicked() {
log(Info, "Plus")
w.Invalidate()
insName, insValue = "", ""
page = insertPage
}
w.Update(ops)
}
}
}
}