From 02f06249091665d10ce533c64aeb74dd326446fe Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sun, 12 Apr 2020 11:47:16 +1000 Subject: [PATCH] allow adding a package --- pkg/commands/os.go | 4 ++ pkg/config/app_config.go | 14 ----- pkg/gui/confirmation_panel.go | 110 ++++++++++++++++++++++------------ pkg/gui/gui.go | 44 +------------- pkg/gui/keybindings.go | 6 ++ pkg/gui/packages_panel.go | 18 +++++- pkg/gui/quitting.go | 2 +- pkg/gui/recent_packages.go | 69 +++++++++++++++++++++ pkg/gui/scripts_panel.go | 10 ++-- pkg/gui/updates.go | 4 +- 10 files changed, 178 insertions(+), 103 deletions(-) create mode 100644 pkg/gui/recent_packages.go diff --git a/pkg/commands/os.go b/pkg/commands/os.go index ce86027..a0d0118 100644 --- a/pkg/commands/os.go +++ b/pkg/commands/os.go @@ -418,5 +418,9 @@ func FileExists(filename string) bool { if os.IsNotExist(err) { return false } + if err != nil { + // should actually do something here + return false + } return !info.IsDir() } diff --git a/pkg/config/app_config.go b/pkg/config/app_config.go index a102ba8..9972f2f 100644 --- a/pkg/config/app_config.go +++ b/pkg/config/app_config.go @@ -22,7 +22,6 @@ type AppConfig struct { UserConfig *viper.Viper UserConfigDir string AppState *AppState - IsNewPackage bool } // AppConfigurer interface allows individual app config structs to inherit Fields @@ -40,8 +39,6 @@ type AppConfigurer interface { WriteToUserConfig(string, interface{}) error SaveAppState() error LoadAppState() error - SetIsNewPackage(bool) - GetIsNewPackage() bool } // NewAppConfig makes a new app config @@ -65,7 +62,6 @@ func NewAppConfig(name, version, commit, date string, buildSource string, debugg UserConfig: userConfig, UserConfigDir: filepath.Dir(userConfigPath), AppState: &AppState{}, - IsNewPackage: false, } if err := appConfig.LoadAppState(); err != nil { @@ -75,16 +71,6 @@ func NewAppConfig(name, version, commit, date string, buildSource string, debugg return appConfig, nil } -// GetIsNewPackage returns known repo boolean -func (c *AppConfig) GetIsNewPackage() bool { - return c.IsNewPackage -} - -// SetIsNewPackage set if the current repo is known -func (c *AppConfig) SetIsNewPackage(isNew bool) { - c.IsNewPackage = isNew -} - // GetDebug returns debug flag func (c *AppConfig) GetDebug() bool { return c.Debug diff --git a/pkg/gui/confirmation_panel.go b/pkg/gui/confirmation_panel.go index c6ea963..26c1cfe 100644 --- a/pkg/gui/confirmation_panel.go +++ b/pkg/gui/confirmation_panel.go @@ -15,11 +15,24 @@ import ( "github.com/jesseduffield/lazynpm/pkg/theme" ) -func (gui *Gui) wrappedConfirmationFunction(function func(*gocui.Gui, *gocui.View) error, returnFocusOnClose bool) func(*gocui.Gui, *gocui.View) error { +func (gui *Gui) wrappedConfirmationFunction(function func() error, returnFocusOnClose bool) func(*gocui.Gui, *gocui.View) error { return func(g *gocui.Gui, v *gocui.View) error { if function != nil { - if err := function(g, v); err != nil { + if err := function(); err != nil { + return err + } + } + + return gui.closeConfirmationPrompt(g, returnFocusOnClose) + } +} + +func (gui *Gui) wrappedPromptConfirmationFunction(function func(string) error, returnFocusOnClose bool) func(*gocui.Gui, *gocui.View) error { + return func(g *gocui.Gui, v *gocui.View) error { + + if function != nil { + if err := function(v.Buffer()); err != nil { return err } } @@ -101,44 +114,68 @@ func (gui *Gui) onNewPopupPanel() { } } -func (gui *Gui) createPopupPanel(g *gocui.Gui, currentView *gocui.View, title, prompt string, hasLoader bool, returnFocusOnClose bool, editable bool, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error { +type createPopupPanelOpts struct { + hasLoader bool + returnFocusOnClose bool + editable bool + prompt string + handleConfirm func() error + handleConfirmPrompt func(string) error + handleClose func() error +} + +func (gui *Gui) createPopupPanel(currentView *gocui.View, title string, opts createPopupPanelOpts) error { gui.onNewPopupPanel() - g.Update(func(g *gocui.Gui) error { + gui.g.Update(func(g *gocui.Gui) error { // delete the existing confirmation panel if it exists if view, _ := g.View("confirmation"); view != nil { if err := gui.closeConfirmationPrompt(g, true); err != nil { gui.Log.Error(err) } } - confirmationView, err := gui.prepareConfirmationPanel(currentView, title, prompt, hasLoader) + confirmationView, err := gui.prepareConfirmationPanel(currentView, title, opts.prompt, opts.hasLoader) if err != nil { return err } - confirmationView.Editable = editable - if editable { + confirmationView.Editable = opts.editable + if opts.editable { confirmationView.EditGotoToEndOfLine() } - gui.renderString("confirmation", prompt) - return gui.setKeyBindings(g, handleConfirm, handleClose, returnFocusOnClose) + gui.renderString("confirmation", opts.prompt) + return gui.setKeyBindings(opts) }) return nil } func (gui *Gui) createLoaderPanel(g *gocui.Gui, currentView *gocui.View, prompt string) error { - return gui.createPopupPanel(g, currentView, "", prompt, true, true, false, nil, nil) + return gui.createPopupPanel(currentView, "", createPopupPanelOpts{ + prompt: prompt, + hasLoader: true, + returnFocusOnClose: true, + }) } // it is very important that within this function we never include the original prompt in any error messages, because it may contain e.g. a user password -func (gui *Gui) createConfirmationPanel(currentView *gocui.View, returnFocusOnClose bool, title, prompt string, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error { - return gui.createPopupPanel(gui.g, currentView, title, prompt, false, returnFocusOnClose, false, handleConfirm, handleClose) +func (gui *Gui) createConfirmationPanel(currentView *gocui.View, returnFocusOnClose bool, title, prompt string, handleConfirm, handleClose func() error) error { + return gui.createPopupPanel(currentView, title, createPopupPanelOpts{ + prompt: prompt, + returnFocusOnClose: returnFocusOnClose, + handleConfirm: handleConfirm, + handleClose: handleClose, + }) } -func (gui *Gui) createPromptPanel(currentView *gocui.View, title string, initialContent string, handleConfirm func(*gocui.Gui, *gocui.View) error) error { - return gui.createPopupPanel(gui.g, currentView, title, initialContent, false, true, true, handleConfirm, nil) +func (gui *Gui) createPromptPanel(currentView *gocui.View, title string, initialContent string, handleConfirm func(string) error) error { + return gui.createPopupPanel(currentView, title, createPopupPanelOpts{ + prompt: initialContent, + returnFocusOnClose: true, + editable: true, + handleConfirmPrompt: handleConfirm, + }) } -func (gui *Gui) setKeyBindings(g *gocui.Gui, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error, returnFocusOnClose bool) error { +func (gui *Gui) setKeyBindings(opts createPopupPanelOpts) error { actions := gui.Tr.TemplateLocalize( "CloseConfirm", Teml{ @@ -148,27 +185,27 @@ func (gui *Gui) setKeyBindings(g *gocui.Gui, handleConfirm, handleClose func(*go ) gui.renderString("options", actions) - if err := g.SetKeybinding("confirmation", nil, gocui.KeyEnter, gocui.ModNone, gui.wrappedConfirmationFunction(handleConfirm, returnFocusOnClose)); err != nil { - return err + if opts.handleConfirmPrompt != nil { + if err := gui.g.SetKeybinding("confirmation", nil, gocui.KeyEnter, gocui.ModNone, gui.wrappedPromptConfirmationFunction(opts.handleConfirmPrompt, opts.returnFocusOnClose)); err != nil { + return err + } + } else { + if err := gui.g.SetKeybinding("confirmation", nil, gocui.KeyEnter, gocui.ModNone, gui.wrappedConfirmationFunction(opts.handleConfirm, opts.returnFocusOnClose)); err != nil { + return err + } } - return g.SetKeybinding("confirmation", nil, gocui.KeyEsc, gocui.ModNone, gui.wrappedConfirmationFunction(handleClose, returnFocusOnClose)) + + return gui.g.SetKeybinding("confirmation", nil, gocui.KeyEsc, gocui.ModNone, gui.wrappedConfirmationFunction(opts.handleClose, opts.returnFocusOnClose)) } -// createSpecificErrorPanel allows you to create an error popup, specifying the -// view to be focused when the user closes the popup, and a boolean specifying -// whether we will log the error. If the message may include a user password, -// this function is to be used over the more generic createErrorPanel, with -// willLog set to false -func (gui *Gui) createSpecificErrorPanel(message string, nextView *gocui.View, willLog bool) error { - if willLog { - go func() { - // when reporting is switched on this log call sometimes introduces - // a delay on the error panel popping up. Here I'm adding a second wait - // so that the error is logged while the user is reading the error message - time.Sleep(time.Second) - gui.Log.Error(message) - }() - } +func (gui *Gui) createErrorPanel(message string) error { + go func() { + // when reporting is switched on this log call sometimes introduces + // a delay on the error panel popping up. Here I'm adding a second wait + // so that the error is logged while the user is reading the error message + time.Sleep(time.Second) + gui.Log.Error(message) + }() colorFunction := color.New(color.FgRed).SprintFunc() coloredMessage := colorFunction(strings.TrimSpace(message)) @@ -176,11 +213,8 @@ func (gui *Gui) createSpecificErrorPanel(message string, nextView *gocui.View, w return err } - return gui.createConfirmationPanel(nextView, true, gui.Tr.SLocalize("Error"), coloredMessage, nil, nil) -} - -func (gui *Gui) createErrorPanel(message string) error { - return gui.createSpecificErrorPanel(message, gui.g.CurrentView(), true) + // TODO: see if returning to the current view is bad in the case of popup views + return gui.createConfirmationPanel(gui.g.CurrentView(), true, gui.Tr.SLocalize("Error"), coloredMessage, nil, nil) } func (gui *Gui) surfaceError(err error) error { diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 7fe49ed..cfd04c8 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -334,44 +334,6 @@ func (gui *Gui) updateRecentPackagesList() error { return errors.New("Must open lazynpm in an npm package") } -func (gui *Gui) sendPackageToTop(path string) error { - // in case we're not already there, chdir to path - if err := os.Chdir(path); err != nil { - return err - } - - recentPackages := gui.Config.GetAppState().RecentPackages - isNew, recentPackages := newRecentPackagesList(recentPackages, path) - gui.Config.SetIsNewPackage(isNew) - gui.Config.GetAppState().RecentPackages = recentPackages - return gui.Config.SaveAppState() -} - -func (gui *Gui) removePackage(path string) error { - recentPackages := gui.Config.GetAppState().RecentPackages - index, ok := utils.StringIndex(recentPackages, path) - if !ok { - return nil - } - recentPackages = append(recentPackages[:index], recentPackages[index+1:]...) - gui.Config.GetAppState().RecentPackages = recentPackages - return gui.Config.SaveAppState() -} - -// newRecentPackagesList returns a new repo list with a new entry but only when it doesn't exist yet -func newRecentPackagesList(recentPackages []string, currentPackage string) (bool, []string) { - isNew := true - newPackages := []string{currentPackage} - for _, pkg := range recentPackages { - if pkg != currentPackage { - newPackages = append(newPackages, pkg) - } else { - isNew = false - } - } - return isNew, newPackages -} - func (gui *Gui) showInitialPopups(tasks []func(chan struct{}) error) { gui.waitForIntro.Add(len(tasks)) done := make(chan struct{}) @@ -391,7 +353,7 @@ func (gui *Gui) showInitialPopups(tasks []func(chan struct{}) error) { } func (gui *Gui) showShamelessSelfPromotionMessage(done chan struct{}) error { - onConfirm := func(g *gocui.Gui, v *gocui.View) error { + onConfirm := func() error { done <- struct{}{} return gui.Config.WriteToUserConfig("startupPopupVersion", StartupPopupVersion) } @@ -400,10 +362,10 @@ func (gui *Gui) showShamelessSelfPromotionMessage(done chan struct{}) error { } func (gui *Gui) promptAnonymousReporting(done chan struct{}) error { - return gui.createConfirmationPanel(nil, true, gui.Tr.SLocalize("AnonymousReportingTitle"), gui.Tr.SLocalize("AnonymousReportingPrompt"), func(g *gocui.Gui, v *gocui.View) error { + return gui.createConfirmationPanel(nil, true, gui.Tr.SLocalize("AnonymousReportingTitle"), gui.Tr.SLocalize("AnonymousReportingPrompt"), func() error { done <- struct{}{} return gui.Config.WriteToUserConfig("reporting", "on") - }, func(g *gocui.Gui, v *gocui.View) error { + }, func() error { done <- struct{}{} return gui.Config.WriteToUserConfig("reporting", "off") }) diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index fc34e67..c016c12 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -440,6 +440,12 @@ func (gui *Gui) GetInitialKeybindings() []*Binding { Modifier: gocui.ModNone, Handler: gui.wrappedHandler(gui.handleCheckoutPackage), }, + { + ViewName: "packages", + Key: gui.getKey("universal.new"), + Modifier: gocui.ModNone, + Handler: gui.wrappedHandler(gui.handleAddPackage), + }, { ViewName: "packages", Key: gui.getKey("packages.link"), diff --git a/pkg/gui/packages_panel.go b/pkg/gui/packages_panel.go index 8e57dbb..1c13442 100644 --- a/pkg/gui/packages_panel.go +++ b/pkg/gui/packages_panel.go @@ -2,6 +2,8 @@ package gui import ( "fmt" + "path/filepath" + "strings" "github.com/fatih/color" "github.com/go-errors/errors" @@ -222,8 +224,22 @@ func (gui *Gui) handleRemovePackage() error { return gui.createErrorPanel("Cannot remove current package") } - return gui.createConfirmationPanel(gui.getPackagesView(), true, "Remove package", "Do you want to remove this package from the list? It won't actually be removed from the filesystem, but as far as lazynpm is concerned it'll be as good as dead. You won't have to worry about it no more.", func(*gocui.Gui, *gocui.View) error { + return gui.createConfirmationPanel(gui.getPackagesView(), true, "Remove package", "Do you want to remove this package from the list? It won't actually be removed from the filesystem, but as far as lazynpm is concerned it'll be as good as dead. You won't have to worry about it no more.", func() error { return gui.removePackage(selectedPkg.Path) }, nil) } + +func (gui *Gui) handleAddPackage() error { + return gui.createPromptPanel(gui.getPackagesView(), "Add package path to add", "", func(input string) error { + configPath := input + if !strings.HasSuffix(configPath, "package.json") { + configPath = filepath.Join(configPath, "package.json") + } + if !commands.FileExists(configPath) { + return gui.createErrorPanel(fmt.Sprintf("%s not found", configPath)) + } + + return gui.addPackage(strings.TrimSuffix(input, "package.json")) + }) +} diff --git a/pkg/gui/quitting.go b/pkg/gui/quitting.go index fa7426c..c566480 100644 --- a/pkg/gui/quitting.go +++ b/pkg/gui/quitting.go @@ -40,7 +40,7 @@ func (gui *Gui) quit(v *gocui.View) error { } if gui.Config.GetUserConfig().GetBool("confirmOnQuit") { - return gui.createConfirmationPanel(v, true, "", gui.Tr.SLocalize("ConfirmQuit"), func(g *gocui.Gui, v *gocui.View) error { + return gui.createConfirmationPanel(v, true, "", gui.Tr.SLocalize("ConfirmQuit"), func() error { return gocui.ErrQuit }, nil) } diff --git a/pkg/gui/recent_packages.go b/pkg/gui/recent_packages.go new file mode 100644 index 0000000..fffb386 --- /dev/null +++ b/pkg/gui/recent_packages.go @@ -0,0 +1,69 @@ +package gui + +import ( + "os" + + "github.com/jesseduffield/lazynpm/pkg/utils" +) + +func (gui *Gui) mutateRecentPackages(f func([]string) ([]string, bool)) error { + recentPackages := gui.Config.GetAppState().RecentPackages + + recentPackages, changed := f(recentPackages) + if !changed { + return nil + } + + gui.Config.GetAppState().RecentPackages = recentPackages + return gui.Config.SaveAppState() + +} + +func (gui *Gui) sendPackageToTop(path string) error { + // in case we're not already there, chdir to path + if err := os.Chdir(path); err != nil { + return err + } + + return gui.mutateRecentPackages(func(recentPackages []string) ([]string, bool) { + updatedRecentPackages := newRecentPackagesList(recentPackages, path) + // just unconditionally saying we updated it even if we didn't + return updatedRecentPackages, true + }) +} + +func (gui *Gui) removePackage(path string) error { + return gui.mutateRecentPackages(func(recentPackages []string) ([]string, bool) { + index, ok := utils.StringIndex(recentPackages, path) + if !ok { + // not removing it if it's already been removed + return nil, false + } + updatedRecentPackages := append(recentPackages[:index], recentPackages[index+1:]...) + return updatedRecentPackages, true + }) +} + +func (gui *Gui) addPackage(path string) error { + return gui.mutateRecentPackages(func(recentPackages []string) ([]string, bool) { + _, ok := utils.StringIndex(recentPackages, path) + if ok { + // not adding it if it's already present + return nil, false + } + updatedRecentPackages := append(recentPackages, path) + return updatedRecentPackages, true + }) +} + +// newRecentPackagesList returns a new repo list with a new entry but only when it doesn't exist yet +// if it already exists, it will be moved to the start of the array +func newRecentPackagesList(recentPackages []string, currentPackage string) []string { + newPackages := []string{currentPackage} + for _, pkg := range recentPackages { + if pkg != currentPackage { + newPackages = append(newPackages, pkg) + } + } + return newPackages +} diff --git a/pkg/gui/scripts_panel.go b/pkg/gui/scripts_panel.go index 5beaf44..cfd913e 100644 --- a/pkg/gui/scripts_panel.go +++ b/pkg/gui/scripts_panel.go @@ -2,7 +2,6 @@ package gui import ( "fmt" - "strings" "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazynpm/pkg/commands" @@ -38,10 +37,9 @@ func (gui *Gui) handleScriptSelect(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleRunScript() error { script := gui.getSelectedScript() - return gui.createPromptPanel(gui.getScriptsView(), "run script", fmt.Sprintf("npm run %s ", script.Name), func(g *gocui.Gui, v *gocui.View) error { - cmdStr := strings.TrimSpace(v.Buffer()) - cmd := gui.OSCommand.ExecutableFromString(cmdStr) - if err := gui.newPtyTask("main", cmd, cmdStr); err != nil { + return gui.createPromptPanel(gui.getScriptsView(), "run script", fmt.Sprintf("npm run %s ", script.Name), func(input string) error { + cmd := gui.OSCommand.ExecutableFromString(input) + if err := gui.newPtyTask("main", cmd, input); err != nil { gui.Log.Error(err) } return nil @@ -51,7 +49,7 @@ func (gui *Gui) handleRunScript() error { func (gui *Gui) handleRemoveScript() error { script := gui.getSelectedScript() - return gui.createConfirmationPanel(gui.getScriptsView(), true, "Remove script", fmt.Sprintf("are you sure you want to remove script `%s`?", script.Name), func(g *gocui.Gui, v *gocui.View) error { + return gui.createConfirmationPanel(gui.getScriptsView(), true, "Remove script", fmt.Sprintf("are you sure you want to remove script `%s`?", script.Name), func() error { return gui.surfaceError( gui.NpmManager.RemoveScript(script.Name, gui.currentPackage().ConfigPath()), ) diff --git a/pkg/gui/updates.go b/pkg/gui/updates.go index 0adb6ed..1fd1db3 100644 --- a/pkg/gui/updates.go +++ b/pkg/gui/updates.go @@ -6,7 +6,7 @@ func (gui *Gui) showUpdatePrompt(newVersion string) error { title := "New version available!" message := "Download latest version? (enter/esc)" currentView := gui.g.CurrentView() - return gui.createConfirmationPanel(currentView, true, title, message, func(g *gocui.Gui, v *gocui.View) error { + return gui.createConfirmationPanel(currentView, true, title, message, func() error { gui.startUpdating(newVersion) return nil }, nil) @@ -57,7 +57,7 @@ func (gui *Gui) onUpdateFinish(err error) error { func (gui *Gui) createUpdateQuitConfirmation(g *gocui.Gui, v *gocui.View) error { title := "Currently Updating" message := "An update is in progress. Are you sure you want to quit?" - return gui.createConfirmationPanel(v, true, title, message, func(g *gocui.Gui, v *gocui.View) error { + return gui.createConfirmationPanel(v, true, title, message, func() error { return gocui.ErrQuit }, nil) }