diff --git a/app/app.go b/app/app.go index b139319b2..1f0dd9343 100644 --- a/app/app.go +++ b/app/app.go @@ -11,6 +11,7 @@ import ( "sync" "time" + "github.com/pkg/errors" "github.com/wailsapp/wails/v2/pkg/mac" "github.com/gregjones/httpcache" @@ -27,6 +28,7 @@ import ( var ( pluginDirectory = filepath.Join(os.Getenv("HOME"), "Library", "Application Support", "xbar", "plugins") cacheDirectory = filepath.Join(os.Getenv("HOME"), "Library", "Application Support", "xbar", "cache") + configFilename = filepath.Join(os.Getenv("HOME"), "Library", "Application Support", "xbar", "xbar.config.json") // concurrentIncomingURLs is the number of concurrent incoming URLs to handle at // the same time. @@ -39,6 +41,8 @@ var ( type app struct { runtime *wails.Runtime + settings *settings + // appMenu isn't visible - it's used for key shortcuts. appMenu *menu.Menu contextMenus []*menu.ContextMenu @@ -47,6 +51,7 @@ type app struct { pluginTrays map[string]*menu.TrayMenu menuParser *MenuParser startsAtLoginMenu *menu.MenuItem + autoupdateMenu *menu.MenuItem appUpdatesMenu *menu.MenuItem // Verbose gets whether verbose output will be printed @@ -80,8 +85,13 @@ type app struct { } // newApp makes a new app. -func newApp() *app { +func newApp() (*app, error) { + settings, err := loadSettings(configFilename) + if err != nil { + return nil, errors.Wrap(err, "loadSettings") + } app := &app{ + settings: settings, Verbose: true, menuParser: NewMenuParser(), incomingURLSemaphore: make(chan struct{}, concurrentIncomingURLs), @@ -113,6 +123,13 @@ func newApp() *app { Click: app.onCheckForUpdatesMenuClick, } + app.autoupdateMenu = &menu.MenuItem{ + Label: "Update automatically", + Type: menu.CheckboxType, + Checked: app.settings.AutoUpdate, + Click: app.updateAutoupdate, + } + app.startsAtLoginMenu = &menu.MenuItem{ Label: "Start at Login", Type: menu.CheckboxType, @@ -145,7 +162,7 @@ func newApp() *app { Label: "xbar", Menu: app.newXbarMenu(nil, false), } - return app + return app, nil } func (app *app) Start(runtime *wails.Runtime) { @@ -168,7 +185,11 @@ func (app *app) Start(runtime *wails.Runtime) { go func() { // wait before checking for updates time.Sleep(10 * time.Second) - app.checkForUpdates(true) + for { + app.checkForUpdates(true) + // check again in twelve hours + time.Sleep(12 * time.Hour) + } }() } @@ -263,6 +284,28 @@ func (app *app) updateStartOnLogin(data *menu.CallbackData) { app.refreshMenus() } +func (app *app) updateAutoupdate(data *menu.CallbackData) { + if data.MenuItem.Checked && app.settings.AutoUpdate { + // nothing to do + return + } + if !data.MenuItem.Checked && !app.settings.AutoUpdate { + // nothing to do + return + } + app.settings.AutoUpdate = data.MenuItem.Checked + if err := app.settings.save(); err != nil { + log.Println("err: updateAutoupdate:", err) + return + } + if app.settings.AutoUpdate { + go app.checkForUpdates(true) + } + // We need to refresh all as the menuitem is used in multiple places. + // If we don't refresh, only the menuitem clicked will toggle in the UI. + app.refreshMenus() +} + // onErr adds a single menu showing the specified error // string. func (app *app) onErr(err string) { @@ -346,6 +389,7 @@ func (app *app) newXbarMenu(plugin *plugins.Plugin, asSubmenu bool) *menu.Menu { Disabled: true, }) items = append(items, app.appUpdatesMenu) + items = append(items, app.autoupdateMenu) items = append(items, app.startsAtLoginMenu) items = append(items, menu.Separator()) items = append(items, &menu.MenuItem{ @@ -561,6 +605,11 @@ func (app *app) updateLabel(tray *menu.TrayMenu, p *plugins.Plugin) bool { // downloads it and installs it. // If passive is true, it won't complain if it fails. func (app *app) checkForUpdates(passive bool) { + if app.Verbose { + log.Printf("checking for updates... (current: %s)", version) + log.Println("updates: passive", passive) + log.Println("updates: AutoUpdate", app.settings.AutoUpdate) + } u := update.Updater{ CurrentVersion: version, //LatestReleaseGitHubEndpoint: "https://api.github.com/repos/matryer/xbar/releases/latest", @@ -574,7 +623,9 @@ func (app *app) checkForUpdates(passive bool) { } latest, hasUpdate, err := u.HasUpdate() if err != nil { - log.Println("failed to check for updates:", err) + if app.Verbose { + log.Println("failed to check for updates:", err) + } if !passive { app.runtime.Dialog.Message(&dialog.MessageDialog{ Type: dialog.ErrorDialog, @@ -588,6 +639,9 @@ func (app *app) checkForUpdates(passive bool) { } if !hasUpdate { // they are using the latest version + if app.Verbose { + log.Println("update: you have the latest version") + } if !passive { app.runtime.Dialog.Message(&dialog.MessageDialog{ Type: dialog.InfoDialog, @@ -599,49 +653,64 @@ func (app *app) checkForUpdates(passive bool) { } return } - oneWeek := 168 * time.Hour - // if this check is passive, and the release is only a few days - // old - do a soft prompt. - if passive && latest.CreatedAt.After(time.Now().Add(0-oneWeek)) { - // Update menu text - app.appUpdatesMenu.Label = "Install " + latest.TagName + "…" - app.refreshMenus() - return - } - switch app.runtime.Dialog.Message(&dialog.MessageDialog{ - Type: dialog.QuestionDialog, - Title: "Update xbar?", - Message: fmt.Sprintf("xbar %s is now available (you have %s).\n\nWould you like to update?", latest.TagName, u.CurrentVersion), - Buttons: []string{"Update", "Later"}, - DefaultButton: "Update", - CancelButton: "Later", - }) { - case "Update": - // continue - case "Later": - return + if !app.settings.AutoUpdate { + oneWeek := 168 * time.Hour + // if this check is passive, and the release is only a few days + // old - do a soft prompt. + if passive && latest.CreatedAt.After(time.Now().Add(0-oneWeek)) { + // Update menu text + app.appUpdatesMenu.Label = "Install " + latest.TagName + "…" + app.refreshMenus() + return + } + switch app.runtime.Dialog.Message(&dialog.MessageDialog{ + Type: dialog.QuestionDialog, + Title: "Update xbar?", + Message: fmt.Sprintf("xbar %s is now available (you have %s).\n\nWould you like to update?", latest.TagName, u.CurrentVersion), + Buttons: []string{"Update", "Later"}, + DefaultButton: "Update", + CancelButton: "Later", + }) { + case "Update": + // continue + case "Later": + return + } + } else { + if app.Verbose { + log.Println("autoupdating...") + } } _, err = u.Update() if err != nil { - app.runtime.Dialog.Message(&dialog.MessageDialog{ - Type: dialog.ErrorDialog, - Title: "Update failed", - Message: err.Error(), - Buttons: []string{"OK"}, - CancelButton: "OK", - }) + if app.Verbose { + log.Println("failed to update:", err) + } + if !passive { + app.runtime.Dialog.Message(&dialog.MessageDialog{ + Type: dialog.InfoDialog, + Title: "Update successful", + Message: "Please restart xbar for the changes to take effect.", + Buttons: []string{"OK"}, + CancelButton: "OK", + }) + } return } err = u.Restart() if err != nil { - log.Println("failed to restart:", err) - app.runtime.Dialog.Message(&dialog.MessageDialog{ - Type: dialog.InfoDialog, - Title: "Update successful", - Message: "Please restart xbar for the changes to take effect.", - Buttons: []string{"OK"}, - CancelButton: "OK", - }) + if app.Verbose { + log.Println("failed to restart:", err) + } + if !passive { + app.runtime.Dialog.Message(&dialog.MessageDialog{ + Type: dialog.InfoDialog, + Title: "Update successful", + Message: "Please restart xbar for the changes to take effect.", + Buttons: []string{"OK"}, + CancelButton: "OK", + }) + } return } } diff --git a/app/go.mod b/app/go.mod index 6bbea082c..4b58ce57a 100644 --- a/app/go.mod +++ b/app/go.mod @@ -12,7 +12,7 @@ require ( github.com/matryer/xbar/pkg/update v0.0.0-00010101000000-000000000000 github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/pkg/errors v0.9.1 - github.com/wailsapp/wails/v2 v2.0.0-alpha.60 + github.com/wailsapp/wails/v2 v2.0.0-alpha.62 golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d // indirect ) diff --git a/app/go.sum b/app/go.sum index 6e7f03858..1c26c7055 100644 --- a/app/go.sum +++ b/app/go.sum @@ -99,8 +99,8 @@ github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLY github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8= github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8= github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= -github.com/wailsapp/wails/v2 v2.0.0-alpha.60 h1:ji3co6B6n0y3Hd0h+fAH8bvE1TPRl7sEQE+Zj89Qr7g= -github.com/wailsapp/wails/v2 v2.0.0-alpha.60/go.mod h1:Yc65JRHZwCwYfBTaemkOzN7nOl2e1iBVZMzEVPxsQ9s= +github.com/wailsapp/wails/v2 v2.0.0-alpha.62 h1:MtcgSY4vVj/Al/wMpyEJZXI2xOGBXGNpH47JSABgRts= +github.com/wailsapp/wails/v2 v2.0.0-alpha.62/go.mod h1:Yc65JRHZwCwYfBTaemkOzN7nOl2e1iBVZMzEVPxsQ9s= github.com/wzshiming/ctc v1.2.3/go.mod h1:2tVAtIY7SUyraSk0JxvwmONNPFL4ARavPuEsg5+KA28= github.com/wzshiming/winseq v0.0.0-20200112104235-db357dc107ae/go.mod h1:VTAq37rkGeV+WOybvZwjXiJOicICdpLCN8ifpISjK20= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= diff --git a/app/main.go b/app/main.go index 550b219ae..e675350b4 100644 --- a/app/main.go +++ b/app/main.go @@ -5,6 +5,7 @@ import ( "fmt" "os" + "github.com/pkg/errors" "github.com/wailsapp/wails/v2" "github.com/wailsapp/wails/v2/pkg/logger" "github.com/wailsapp/wails/v2/pkg/options" @@ -24,13 +25,16 @@ func main() { } func run() error { - app := newApp() + app, err := newApp() + if err != nil { + return errors.Wrap(err, "newApp") + } wailsLogLevel := logger.ERROR app.Verbose = true if app.Verbose { wailsLogLevel = logger.DEBUG } - err := wails.Run(&options.App{ + err = wails.Run(&options.App{ Title: "xbar", Width: 1080, Height: 700, diff --git a/app/package.sh b/app/package.sh index 00ebcaa07..f9fc5ef11 100755 --- a/app/package.sh +++ b/app/package.sh @@ -17,6 +17,8 @@ echo -n $VERSION > .version # run all tests ./test.sh +rm -rf ./build + sed "s/0.0.0/${VERSION}/" ./assets/mac/info.plist.src > ./assets/mac/info.plist CGO_LDFLAGS=-mmacosx-version-min=10.13 wails build -package -production -platform darwin/amd64 #CGO_LDFLAGS=-mmacosx-version-min=10.13 wails build -package -production -platform darwin/arm64 diff --git a/app/plugins_service.go b/app/plugins_service.go index 0277f3b33..1371a5245 100644 --- a/app/plugins_service.go +++ b/app/plugins_service.go @@ -29,6 +29,7 @@ type PluginsService struct { // osLock is used whenever there are operating system changes, // like renaming files. This prevents overlap and potentially strange // state. + // todo: move this to a better place. osLock sync.Mutex // OnRefresh is called whenever the menus should diff --git a/app/settings.go b/app/settings.go new file mode 100644 index 000000000..52001e8de --- /dev/null +++ b/app/settings.go @@ -0,0 +1,53 @@ +package main + +import ( + "encoding/json" + "io/ioutil" + "os" + "path/filepath" + + "github.com/pkg/errors" +) + +type settings struct { + path string `json:"-"` + + // AutoUpdate indicates that xbar should automatically + // update itself. + AutoUpdate bool `json:"autoupdate"` +} + +func loadSettings(path string) (*settings, error) { + s := &settings{ + path: path, + } + b, err := ioutil.ReadFile(path) + if err != nil { + if os.IsNotExist(err) { + // file not found - it's ok, just use defaults + return s, nil + } + return nil, errors.Wrap(err, "ReadFile") + } + err = json.Unmarshal(b, s) + if err != nil { + return nil, errors.Wrap(err, "Unmarshal") + } + return s, nil +} + +func (s *settings) save() error { + b, err := json.MarshalIndent(s, "", "\t") + if err != nil { + return errors.Wrap(err, "MarshalIndent") + } + err = os.MkdirAll(filepath.Dir(s.path), 0777) + if err != nil { + return errors.Wrap(err, "MkdirAll") + } + err = ioutil.WriteFile(s.path, b, 0777) + if err != nil { + return errors.Wrap(err, "WriteFile") + } + return nil +} diff --git a/app/settings_test.go b/app/settings_test.go new file mode 100644 index 000000000..1d5a2160d --- /dev/null +++ b/app/settings_test.go @@ -0,0 +1,29 @@ +package main + +import ( + "os" + "path/filepath" + "testing" + + "github.com/matryer/is" +) + +func TestSettings(t *testing.T) { + is := is.New(t) + + t.Cleanup(func() { + os.RemoveAll(filepath.Join("testdata", "settings.json")) + }) + + s, err := loadSettings(filepath.Join("testdata", "settings.json")) + is.NoErr(err) + s.AutoUpdate = true + + err = s.save() + is.NoErr(err) + + s, err = loadSettings(filepath.Join("testdata", "settings.json")) + is.NoErr(err) + is.Equal(s.AutoUpdate, true) + +} diff --git a/pkg/update/update_test.go b/pkg/update/update_test.go index 247229e3b..eece1697b 100644 --- a/pkg/update/update_test.go +++ b/pkg/update/update_test.go @@ -77,7 +77,8 @@ func TestUpdate(t *testing.T) { apiServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { version := "v2.0.0" response := Release{ - TagName: version, + CreatedAtString: time.Now().Format(time.RFC3339Nano), + TagName: version, Assets: []Asset{ { Name: "xbar-" + version + ".tar.gz", @@ -129,7 +130,8 @@ func TestMatchingVersions(t *testing.T) { apiServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { version := "v2.1.0" response := Release{ - TagName: version, + CreatedAtString: time.Now().Format(time.RFC3339Nano), + TagName: version, Assets: []Asset{ { Name: "xbar-" + version + ".tar.gz", @@ -176,7 +178,8 @@ func TestLocalVersionIsHigher(t *testing.T) { apiServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { version := "v2.1.0" response := Release{ - TagName: version, + CreatedAtString: time.Now().Format(time.RFC3339Nano), + TagName: version, Assets: []Asset{ { Name: "xbar-" + version + ".tar.gz",