passgo/cmd/passgo-gui/main.go
Greg Pomerantz 349445219b Android: reliable build script; GUI: fix list not drawn until first tap.
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.
2026-08-24 09:23:41 -04:00

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)
}
}
}