scripts/build.sh (documented in README) builds, manifest-patches ( MANAGE_EXTERNAL_STORAGE ), signs and installs the APK in one go. Without the permission the system 'All files access' toggle is grayed out on Android 11+. GUI fix: the event loop only checks the 'updated' channel when it is not blocked in w.Event(), and gioui only emits frames on invalidation or user input, so the initial list load was never drawn until a tap. The updater now signals non-blocking and invalidates the window itself; the animation ticker gets the same wake-up treatment.
1010 lines
24 KiB
Go
1010 lines
24 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"path"
|
|
"runtime"
|
|
"runtime/pprof"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"gioui.org/app"
|
|
"gioui.org/font/gofont"
|
|
"gioui.org/layout"
|
|
"gioui.org/op"
|
|
"gioui.org/text"
|
|
"gioui.org/unit"
|
|
"gioui.org/widget"
|
|
"gioui.org/widget/material"
|
|
|
|
"github.com/fsnotify/fsnotify"
|
|
"gopkg.in/yaml.v2"
|
|
|
|
"git.wow.st/gmp/passgo"
|
|
"git.wow.st/gmp/rand"
|
|
)
|
|
|
|
type (
|
|
D = layout.Dimensions
|
|
C = layout.Context
|
|
)
|
|
|
|
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{}, 1)
|
|
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
|
|
collection []text.FontFace
|
|
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
|
|
win atomic.Pointer[app.Window]
|
|
)
|
|
|
|
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()
|
|
// Non-blocking: if a refresh is already pending it will pick up
|
|
// the latest list, so dropping the duplicate is fine.
|
|
select {
|
|
case updated <- struct{}{}:
|
|
default:
|
|
}
|
|
// Wake the window: the event loop blocks in w.Event(), which only
|
|
// returns when the window is invalidated or the user interacts
|
|
// with it. Without this, a list refresh signalled while the loop
|
|
// is blocked (e.g. the initial load on startup) would not be
|
|
// drawn until the next tap.
|
|
if w := win.Load(); w != nil {
|
|
w.Invalidate()
|
|
}
|
|
}
|
|
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() {
|
|
var ops op.Ops
|
|
th = material.NewTheme()
|
|
th.TextSize = unit.Sp(fontSize)
|
|
if len(collection) > 0 {
|
|
// Use the embedded Go font plus the platform fonts (e.g. the
|
|
// Unicode font installed on Darwin) instead of the system fonts.
|
|
th.Shaper = text.NewShaper(text.NoSystemFonts(),
|
|
text.WithCollection(append(gofont.Collection(), collection...)))
|
|
} else {
|
|
// Always provide the embedded Go font: the system font scan
|
|
// finds no fonts on Android, and a fontless shaper renders a
|
|
// blank screen.
|
|
th.Shaper = text.NewShaper(text.WithCollection(gofont.Collection()))
|
|
}
|
|
w := new(app.Window)
|
|
w.Option(
|
|
app.Size(unit.Dp(250), unit.Dp(500)),
|
|
app.Title("passgo"))
|
|
win.Store(w)
|
|
|
|
var margincs layout.Constraints
|
|
|
|
var c1 layout.FlexChild // flex child for title bar
|
|
|
|
margin := layout.UniformInset(unit.Dp(10))
|
|
|
|
title := material.Body1(th, "passgo")
|
|
dotsBtn := &Button{
|
|
Size: unit.Sp(fontSize),
|
|
Label: "\u2026",
|
|
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
|
|
|
|
FilterEd := &widget.Editor{SingleLine: true}
|
|
var lastFilter string
|
|
updateBtns := func() {
|
|
passBtns = passBtns[:0]
|
|
pathnames = pathnames[:0]
|
|
dirs := make(map[string]bool) // visited pathnames
|
|
|
|
var addd, addf func(x passgo.Pass)
|
|
addd = func(x passgo.Pass) {
|
|
if x.Level < 1 {
|
|
return
|
|
}
|
|
d, n := path.Split(x.Pathname)
|
|
x = passgo.Pass{
|
|
Pathname: d,
|
|
Level: x.Level - 1,
|
|
Dir: true,
|
|
}
|
|
fmt.Printf("addd(): d/n = '%s'/'%s'\n", d, n)
|
|
if dirs[d] != true {
|
|
addd(x)
|
|
}
|
|
dirs[x.Pathname] = true
|
|
s := strings.Repeat(" /", x.Level)
|
|
lbl := strings.Join([]string{s, d}, "")
|
|
passBtns = append(passBtns, &Button{
|
|
Size: unit.Sp(fontSize),
|
|
Label: lbl,
|
|
Color: black,
|
|
Background: gray,
|
|
})
|
|
pathnames = append(pathnames, x.Pathname)
|
|
}
|
|
|
|
flt := strings.ToUpper(FilterEd.Text())
|
|
addf = func(x passgo.Pass) {
|
|
if !strings.Contains(strings.ToUpper(x.Pathname), flt) {
|
|
return
|
|
}
|
|
d, n := path.Split(x.Pathname)
|
|
s := strings.Repeat(" /", x.Level)
|
|
lbl := strings.Join([]string{s, n}, "")
|
|
if dirs[d] != true {
|
|
addd(x)
|
|
}
|
|
passBtns = append(passBtns, &Button{
|
|
Size: unit.Sp(fontSize),
|
|
Label: lbl,
|
|
Color: black,
|
|
Background: gray,
|
|
})
|
|
pathnames = append(pathnames, x.Pathname)
|
|
}
|
|
mux.Lock()
|
|
for _, x := range l {
|
|
if !x.Dir {
|
|
addf(x)
|
|
}
|
|
}
|
|
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 := material.Label(th, 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 := material.Label(th, unit.Sp(fontSize), "Password exists. Overwrite?")
|
|
yesBtn := &Button{
|
|
Size: unit.Sp(fontSize),
|
|
Label: "yes",
|
|
Alignment: text.End,
|
|
Color: black,
|
|
Background: gray,
|
|
}
|
|
|
|
promptLabel := material.Label(th, 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,
|
|
}
|
|
xBtn := &Button{
|
|
Size: unit.Sp(fontSize),
|
|
Label: "X",
|
|
Alignment: text.Middle,
|
|
Color: black,
|
|
Background: gray,
|
|
}
|
|
|
|
insertLabel := material.Label(th, unit.Sp(fontSize), "Insert")
|
|
passnameLabel := material.Label(th, unit.Sp(fontSize), "password name:")
|
|
passnameEd := &widget.Editor{SingleLine: true}
|
|
passvalLabel := material.Label(th, unit.Sp(fontSize), "password value:")
|
|
passvalEd := &widget.Editor{SingleLine: true, Submit: true}
|
|
|
|
noidLabel := material.Label(th, unit.Sp(fontSize), noidLabelText)
|
|
idLabel := material.Label(th, unit.Sp(fontSize), "Select ID")
|
|
|
|
// timing variables used for the "copied to clipboard" animation
|
|
// (seconds relative to overlayStart)
|
|
fade1a, fade1b := 1.5, 2.0
|
|
start2 := float64(Config.ClearDelay)
|
|
fade2a, end := start2+1.5, start2+2.0
|
|
|
|
anim := &time.Ticker{}
|
|
var animDone chan struct{}
|
|
animating := false
|
|
animOn := func() {
|
|
log(Info, "animOn()")
|
|
anim = time.NewTicker(time.Second / 90)
|
|
animating = true
|
|
w.Invalidate()
|
|
// The event loop blocks in w.Event() between frames, so ticks it
|
|
// misses never trigger a redraw and the fade would freeze. Wake
|
|
// the window on each tick, but only while a fade is actually
|
|
// visible.
|
|
t := anim
|
|
animDone = make(chan struct{})
|
|
d := animDone
|
|
go func() {
|
|
for {
|
|
select {
|
|
case <-t.C:
|
|
x := time.Since(overlayStart).Seconds()
|
|
if (x >= fade1a && x < fade1b) || (x > fade2a && x < end) {
|
|
w.Invalidate()
|
|
}
|
|
case <-d:
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
animOff := func() {
|
|
log(Info, "animOff()")
|
|
anim.Stop()
|
|
animating = false
|
|
if animDone != nil { // stops the wake-up goroutine started by animOn
|
|
close(animDone)
|
|
animDone = nil
|
|
}
|
|
}
|
|
|
|
var listPage, idPage, insertPage, confirmPage, confPage, promptPage, page func(C) D
|
|
_ = idPage
|
|
|
|
prompt := func() []byte {
|
|
page = promptPage
|
|
promptEd.SetText("")
|
|
w.Invalidate()
|
|
return <-passch
|
|
}
|
|
|
|
lpf1 := &layout.Flex{Axis: layout.Horizontal}
|
|
listPage = func(gtx C) D {
|
|
c2 := layout.Rigid(func(gtx C) D {
|
|
c21 := layout.Flexed(1, func(gtx C) D {
|
|
return material.Editor(th, FilterEd, "filter").Layout(gtx)
|
|
})
|
|
c22 := layout.Rigid(func(gtx C) D {
|
|
return xBtn.Layout(gtx)
|
|
})
|
|
if xBtn.Clicked() {
|
|
FilterEd.SetText("")
|
|
lastFilter = ""
|
|
updateBtns()
|
|
}
|
|
return lpf1.Layout(gtx, c21, c22)
|
|
})
|
|
c3 := layout.Flexed(1.0, func(gtx C) D {
|
|
var ret D
|
|
mux.Lock()
|
|
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:
|
|
ret = lst.Layout(gtx, len(passBtns), func(gtx C, i int) D {
|
|
btn := passBtns[i]
|
|
gtx.Constraints.Min.X = gtx.Constraints.Max.X
|
|
ret := 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])
|
|
}
|
|
return ret
|
|
})
|
|
}
|
|
mux.Unlock()
|
|
return ret
|
|
})
|
|
ret := flex.Layout(gtx, c1, c2, c3)
|
|
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
|
|
layout.SE.Layout(gtx, func(gtx C) D {
|
|
gtx.Constraints.Min.X = gtx.Constraints.Max.X
|
|
return overlay.Layout(gtx)
|
|
})
|
|
}
|
|
if x > start2 && x < fade2a {
|
|
// animOff()
|
|
}
|
|
if animating && x > end {
|
|
animOff()
|
|
}
|
|
// material.Editor.Layout (above) already consumes the editor's
|
|
// input events, so we detect a filter change by comparing the
|
|
// current text against the previous frame's.
|
|
if t := FilterEd.Text(); t != lastFilter {
|
|
lastFilter = t
|
|
updateBtns()
|
|
}
|
|
return ret
|
|
}
|
|
|
|
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(gtx C) D {
|
|
if !animating {
|
|
animOn()
|
|
}
|
|
c2 := layout.Rigid(func(gtx C) D {
|
|
return 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 = layout.Rigid(func(gtx C) D {
|
|
return updateBtn.Layout(gtx)
|
|
})
|
|
if updateBtn.Clicked() {
|
|
updateIdBtns()
|
|
w.Invalidate()
|
|
}
|
|
c4 = layout.Rigid(func(gtx C) D {
|
|
return material.Editor(th, idEd, "id").Layout(gtx)
|
|
})
|
|
c5 = layout.Rigid(func(gtx C) D {
|
|
return idSubmitBtn.Layout(gtx)
|
|
})
|
|
if idSubmitBtn.Clicked() {
|
|
store.Id = idEd.Text()
|
|
// Persist the ID so it survives a restart (the manual entry
|
|
// path has no keyring to re-derive it from).
|
|
store.SetID(store.Id)
|
|
page = listPage
|
|
}
|
|
c6 = layout.Rigid(func(gtx C) D {
|
|
return noidLabel.Layout(gtx)
|
|
})
|
|
} else {
|
|
c3 = layout.Rigid(func(gtx C) D { return D{} })
|
|
c4 = layout.Rigid(func(gtx C) D { return D{} })
|
|
c5 = layout.Rigid(func(gtx C) D { return D{} })
|
|
c6 = layout.Rigid(func(gtx C) D { return D{} })
|
|
}
|
|
c7 = layout.Rigid(func(gtx C) D {
|
|
var ret D
|
|
if len(idBtns) > 0 { // still zero after update
|
|
for i := 0; i < len(idBtns); i++ {
|
|
ret = lst.Layout(gtx, len(idBtns), func(gtx C, i int) D {
|
|
return 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
|
|
}
|
|
}
|
|
}
|
|
return ret
|
|
})
|
|
if ev, ok := idEd.Update(gtx); ok {
|
|
if _, isSubmit := ev.(widget.SubmitEvent); isSubmit {
|
|
log(Info, "Submit")
|
|
store.Id = idEd.Text()
|
|
store.SetID(store.Id)
|
|
w.Invalidate()
|
|
page = listPage
|
|
}
|
|
}
|
|
return 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", Color: black}
|
|
symBtn := &SelButton{SelColor: gray}
|
|
numBtn := &SelButton{SelColor: gray}
|
|
symBtn.Button = Button{Size: unit.Sp(fontSize), Label: "@", Color: black}
|
|
numBtn.Button = Button{Size: unit.Sp(fontSize), Label: "#", Color: black}
|
|
symBtn.Select()
|
|
numBtn.Select()
|
|
lenEd := &widget.Editor{SingleLine: true, Alignment: text.End}
|
|
lenEd.SetText("15")
|
|
lBtn := &Button{Size: unit.Sp(fontSize), Label: "<", Color: black, Background: gray}
|
|
rBtn := &Button{Size: unit.Sp(fontSize), Label: ">", Color: black, 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(gtx C) D {
|
|
c2 := layout.Rigid(func(gtx C) D { return insertLabel.Layout(gtx) })
|
|
c3 := layout.Rigid(func(gtx C) D { return passnameLabel.Layout(gtx) })
|
|
c4 := layout.Rigid(func(gtx C) D { return material.Editor(th, passnameEd, "name").Layout(gtx) })
|
|
c5 := layout.Rigid(func(gtx C) D { return passvalLabel.Layout(gtx) })
|
|
c6 := layout.Rigid(func(gtx C) D { return material.Editor(th, passvalEd, "password").Layout(gtx) })
|
|
|
|
btnflx := &layout.Flex{Axis: layout.Horizontal}
|
|
c7 := layout.Rigid(func(gtx C) D {
|
|
bc1 := layout.Rigid(func(gtx C) D { return lBtn.Layout(gtx) })
|
|
bc2 := layout.Rigid(func(gtx C) D {
|
|
gtx.Constraints.Min.X = 60
|
|
return material.Editor(th, lenEd, "len").Layout(gtx)
|
|
})
|
|
|
|
bc3 := layout.Rigid(func(gtx C) D { return rBtn.Layout(gtx) })
|
|
bc4 := layout.Rigid(func(gtx C) D { return symBtn.Layout(gtx) })
|
|
bc5 := layout.Rigid(func(gtx C) D { return numBtn.Layout(gtx) })
|
|
bc6 := layout.Rigid(func(gtx C) D { return genBtn.Layout(gtx) })
|
|
|
|
return layout.E.Layout(gtx, func(gtx C) D {
|
|
return btnflx.Layout(gtx, bc1, bc2, bc3, bc4, bc5, bc6)
|
|
})
|
|
})
|
|
|
|
c8 := layout.Rigid(func(gtx C) D {
|
|
bc1 := layout.Rigid(func(gtx C) D { return backBtn.Layout(gtx) })
|
|
bc2 := layout.Rigid(func(gtx C) D { return saveBtn.Layout(gtx) })
|
|
return layout.E.Layout(gtx, func(gtx C) D {
|
|
return btnflx.Layout(gtx, bc1, bc2)
|
|
})
|
|
})
|
|
|
|
ret := 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 ret
|
|
}
|
|
}
|
|
//Do not block the UI thread.
|
|
go func() {
|
|
err := store.Insert(passnameEd.Text(), passvalEd.Text())
|
|
if err != nil {
|
|
page = idPage
|
|
}
|
|
}()
|
|
}
|
|
return ret
|
|
}
|
|
|
|
confirmPage = func(gtx C) D {
|
|
c2 := layout.Rigid(func(gtx C) D {
|
|
return confirmLabel.Layout(gtx)
|
|
})
|
|
btnflx := &layout.Flex{Axis: layout.Horizontal}
|
|
c3 := layout.Rigid(func(gtx C) D {
|
|
bc1 := layout.Rigid(func(gtx C) D { return backBtn.Layout(gtx) })
|
|
bc2 := layout.Rigid(func(gtx C) D { return yesBtn.Layout(gtx) })
|
|
|
|
return layout.E.Layout(gtx, func(gtx C) D {
|
|
return btnflx.Layout(gtx, bc1, bc2)
|
|
})
|
|
})
|
|
ret := 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
|
|
}
|
|
}()
|
|
}
|
|
return ret
|
|
}
|
|
|
|
confPage = func(gtx C) D {
|
|
c2 := layout.Rigid(func(gtx C) D { return storeDirLabel.Layout(gtx) })
|
|
c3 := layout.Rigid(func(gtx C) D { return material.Editor(th, storeDirEd, "directory").Layout(gtx) })
|
|
|
|
c4 := layout.Rigid(func(gtx C) D {
|
|
btnflx := &layout.Flex{Axis: layout.Horizontal}
|
|
bc1 := layout.Rigid(func(gtx C) D {
|
|
return backBtn.Layout(gtx)
|
|
})
|
|
bc2 := layout.Rigid(func(gtx C) D {
|
|
return saveBtn.Layout(gtx)
|
|
})
|
|
return layout.E.Layout(gtx, func(gtx C) D {
|
|
return btnflx.Layout(gtx, bc1, bc2)
|
|
})
|
|
})
|
|
ret := 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
|
|
}
|
|
return ret
|
|
}
|
|
|
|
promptPage = func(gtx C) D {
|
|
submit := false
|
|
if ev, ok := promptEd.Update(gtx); ok {
|
|
if _, isSubmit := ev.(widget.SubmitEvent); isSubmit {
|
|
log(Info, "Submit")
|
|
submit = true
|
|
}
|
|
}
|
|
c2 := layout.Rigid(func(gtx C) D { return promptLabel.Layout(gtx) })
|
|
c3 := layout.Rigid(func(gtx C) D { return material.Editor(th, promptEd, "password").Layout(gtx) })
|
|
c4 := layout.Rigid(func(gtx C) D {
|
|
btnflx := &layout.Flex{Axis: layout.Horizontal}
|
|
bc1 := layout.Rigid(func(gtx C) D {
|
|
return backBtn.Layout(gtx)
|
|
})
|
|
bc2 := layout.Rigid(func(gtx C) D {
|
|
return okBtn.Layout(gtx)
|
|
})
|
|
return layout.E.Layout(gtx, func(gtx C) D {
|
|
return btnflx.Layout(gtx, bc1, bc2)
|
|
})
|
|
})
|
|
ret := 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
|
|
}
|
|
return ret
|
|
}
|
|
|
|
page = listPage
|
|
|
|
ms := &runtime.MemStats{}
|
|
x := 0
|
|
var mallocs uint64
|
|
var idsInit bool
|
|
for {
|
|
// Window.Event and FrameEvent.Frame must be called from the same
|
|
// goroutine; drain the auxiliary channels non-blocking instead of
|
|
// bridging the window events through a channel.
|
|
select {
|
|
case <-updated:
|
|
log(Info, "UPDATE")
|
|
updateBtns()
|
|
w.Invalidate()
|
|
default:
|
|
}
|
|
select {
|
|
case <-anim.C:
|
|
w.Invalidate()
|
|
default:
|
|
}
|
|
switch e := w.Event().(type) {
|
|
case app.DestroyEvent:
|
|
os.Exit(0)
|
|
case app.FrameEvent:
|
|
x++
|
|
if !idsInit { // the app is running; refresh the ID list
|
|
idsInit = true
|
|
go updateIdBtns()
|
|
}
|
|
// NewContext resets ops and adjusts for e.Insets.
|
|
gtx := app.NewContext(&ops, e)
|
|
|
|
margin.Layout(gtx, func(gtx C) D {
|
|
margincs = gtx.Constraints
|
|
c1 = layout.Rigid(func(gtx C) D {
|
|
ct2 := layout.Rigid(func(gtx C) D {
|
|
return plusBtn.Layout(gtx)
|
|
})
|
|
ct3 := layout.Rigid(func(gtx C) D {
|
|
return dotsBtn.Layout(gtx)
|
|
})
|
|
ct1 := layout.Flexed(1.0, func(gtx C) D {
|
|
gtx.Constraints.Min.X = gtx.Constraints.Max.X
|
|
return title.Layout(gtx)
|
|
})
|
|
return titleflex.Layout(gtx, ct1, ct2, ct3)
|
|
})
|
|
|
|
return page(gtx)
|
|
})
|
|
|
|
if dotsBtn.Clicked() {
|
|
log(Info, "Configure")
|
|
w.Invalidate()
|
|
page = confPage
|
|
}
|
|
if plusBtn.Clicked() {
|
|
log(Info, "Plus")
|
|
w.Invalidate()
|
|
insName, insValue = "", ""
|
|
page = insertPage
|
|
}
|
|
e.Frame(&ops)
|
|
default:
|
|
handleEvent(e)
|
|
}
|
|
if x == 100 {
|
|
x = 0
|
|
runtime.ReadMemStats(ms)
|
|
fmt.Printf("mallocs: %d\n", ms.Mallocs-mallocs)
|
|
}
|
|
}
|
|
}
|