From 0974e603a77ed418af1bae6f85348d04a7c67b16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Sauvage?= Date: Fri, 24 Jun 2022 18:47:37 +0200 Subject: [PATCH 01/42] Add Keybindings_fr.md --- docs/keybindings/Keybindings_fr.md | 75 ++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 docs/keybindings/Keybindings_fr.md diff --git a/docs/keybindings/Keybindings_fr.md b/docs/keybindings/Keybindings_fr.md new file mode 100644 index 00000000..124e0b51 --- /dev/null +++ b/docs/keybindings/Keybindings_fr.md @@ -0,0 +1,75 @@ +# Menu Lazydocker + +## Projet + +
+  e: modifier la configuration lazydocker
+  o: ouvrir la configuration lazydocker
+  [: onglet précédent
+  ]: onglet suivant
+  m: voir le journal
+  enter: mise au point sur le paneau principale
+
+ +## Conteneurs + +
+  [: onglet précédent
+  ]: onglet suivant
+  d: retirer
+  e: Cacher/Montrer les conteneurs arrêtés
+  s: arrêter
+  r: redémarrer
+  a: attacher
+  m: voir le journal
+  E: shell d'exécution
+  c: lancer une commande pérsonnalisée prédéfinie
+  b: voir les commandes groupés
+  w: ouvrir dans le navigateur (le premier port est http)
+  enter: mise au point sur le paneau principale
+
+ +## Services + +
+  d: retirer les conteneurs
+  s: arrêter
+  r: redémarrer
+  a: attacher
+  m: voir le journal
+  [: onglet précédent
+  ]: onglet suivant
+  R: voir les options de redémarrage
+  c: lancer une commande pérsonnalisée prédéfinie
+  b: voir les commandes groupés
+  w: ouvrir dans le navigateur (le premier port est http)
+  enter: mise au point sur le paneau principale
+
+ +## Images + +
+  [: onglet précédent
+  ]: onglet suivant
+  c: lancer une commande pérsonnalisée prédéfinie
+  d: retirer l'image
+  b: voir les commandes groupés
+  enter: mise au point sur le paneau principale
+
+ +## Volumes + +
+  [: onglet précédent
+  ]: onglet suivant
+  c: lancer une commande pérsonnalisée prédéfinie
+  d: retirer le volume
+  b: voir les commandes groupés
+  enter: mise au point sur le paneau principale
+
+ +## Principale + +
+  esc: retour
+
From ca0956137bc87a397b37cac566ee35e1bdadfe3c Mon Sep 17 00:00:00 2001 From: Clement Sauvage Date: Tue, 11 Oct 2022 21:11:03 +0200 Subject: [PATCH 02/42] Add pkg/i18n/french.go --- pkg/i18n/french.go | 115 +++++++++++++++++++++++++++++++++++++++++++++ pkg/i18n/i18n.go | 1 + 2 files changed, 116 insertions(+) create mode 100644 pkg/i18n/french.go diff --git a/pkg/i18n/french.go b/pkg/i18n/french.go new file mode 100644 index 00000000..82c34252 --- /dev/null +++ b/pkg/i18n/french.go @@ -0,0 +1,115 @@ +package i18n + +func frenchSet() TranslationSet { + return TranslationSet{ + PruningStatus: "destruction", + RemovingStatus: "supression", + RestartingStatus: "redémarrage", + StartingStatus: "démarrage", + StoppingStatus: "arrêt", + PausingStatus: "mise en pause", + RunningCustomCommandStatus: "execution de la commande personalisée", + RunningBulkCommandStatus: "execution de la commande groupée", + + RunningSubprocess: "execution du sous-processus", + NoViewMachingNewLineFocusedSwitchStatement: "No view matching newLineFocused switch statement", + + ErrorOccurred: "Une erreur s'est produite! Veuillez créer un rapport d'erreur sur https://github.com/jesseduffield/lazydocker/issues", + ConnectionFailed: "Erreur lors de la connexion au client docker. Essayez de redémarrer votre client docker", + UnattachableContainerError: "Le conteneur ne peut pas être attaché. Vous devez executer le service avec le drapeau 'it' ou bien utiliser `stdin_open: true, tty: true` dans votre fichier docker-compose.yaml", + WaitingForContainerInfo: "Le processus ne peut pas continuer avant que docker donne plus d'informations. Veuillez réessayer dans quelques instants.", + + CannotAttachStoppedContainerError: "Vous ne pouvez pas vous attacher à un conteneur en arrêt, vous devez le démarrer avant (ce que vous pouvez faire avec la touche 'r') (oui, je suis trop paresseux pour le faire automatiquement pour vous) (plutôt cool que je puisse communiquer en tête-à-tête avec vous au travers d'un message d'erreur)", + CannotAccessDockerSocketError: "Impossible d'accèder à la socket docker à: unix:///var/run/docker.sock\nLancez lazydocker en tant que root ou alors lisez https://docs.docker.com/install/linux/linux-postinstall/", + CannotKillChildError: "Trois secondes ont étés attendu pour l'arrêt des processus fils. Il y a possiblement un processus orphelin qui continue à tourner sur votre systeme.", + + Donate: "Donner", + Confirm: "Confirmer", + + Return: "retour", + FocusMain: "focus paneau principal", + Navigate: "naviguer", + Execute: "executer", + Close: "fermer", + Menu: "menu", + MenuTitle: "Menu", + Scroll: "faire défiler", + OpenConfig: "ouvrire la configuration lazydocker", + EditConfig: "modifier la configuration lazydocker", + Cancel: "annuler", + Remove: "supprimer", + HideStopped: "cacher/montrer les conteneurs arrêtés", + ForceRemove: "forcer la supression", + RemoveWithVolumes: "supprimer avec les volumes", + RemoveService: "supprimer les conteneurs", + Stop: "arrêter", + Pause: "pause", + Restart: "redémarrer", + Start: "démarrer", + Rebuild: "reconstruire", + Recreate: "recréer", + PreviousContext: "onglet précédent", + NextContext: "onglet suivant", + Attach: "attacher", + ViewLogs: "voir les enregistrements", + RemoveImage: "supprimer l'image", + RemoveVolume: "supprimer le volume", + RemoveWithoutPrune: "supprimer sans effacer les parents non étiqueté", + RemoveWithoutPruneWithForce: "supprimer (forcer) sans effacer les parents non étiqueté", + RemoveWithForce: "supprimer (forcer)", + PruneContainers: "détruire les conteneurs arrêtes", + PruneVolumes: "détruire les volumes non utilisés", + PruneImages: "détruire les images non utilisés", + StopAllContainers: "arrêter tous les conteneurs", + RemoveAllContainers: "supprimer tous les conteneurs (forcer)", + ViewRestartOptions: "voir les options de redémarrage", + ExecShell: "executer le shell", + RunCustomCommand: "executer une commande prédéfinie", + ViewBulkCommands: "voir les commandes groupés", + OpenInBrowser: "ouvrir dans le navgateur (le premier port est http)", + SortContainersByState: "ordonner les conteneurs par état", + + GlobalTitle: "Global", + MainTitle: "Principal", + ProjectTitle: "Projet", + ServicesTitle: "Services", + ContainersTitle: "Conteneurs", + StandaloneContainersTitle: "Conteneurs Autonomes", + ImagesTitle: "Images", + VolumesTitle: "Volumes", + CustomCommandTitle: "Commande personalisé:", + BulkCommandTitle: "Commande groupée:", + ErrorTitle: "Erreur", + LogsTitle: "Enregistrements", + ConfigTitle: "Config", + EnvTitle: "Env", + DockerComposeConfigTitle: "Config Docker-Compose", + TopTitle: "Top", + StatsTitle: "Statistiques", + CreditsTitle: "A propos", + ContainerConfigTitle: "Config Conteneur", + ContainerEnvTitle: "Env Conteneur", + NothingToDisplay: "Rien à afficher", + CannotDisplayEnvVariables: "Quelque chose à échoué lors de l'affichage des variables d'environnement", + + NoContainers: "Aucun conteneurs", + NoContainer: "Aucun conteneur", + NoImages: "Aucune images", + NoVolumes: "Aucun volumes", + + ConfirmQuit: "Êtes vous certain de vouloir quitter?", + MustForceToRemoveContainer: "Vous ne pouvez pas supprimer un conteneur qui tourne sans le forcer. Voulez-vous le forcer ?", + NotEnoughSpace: "Manque d'espace pour afficher les differents panneaux", + ConfirmPruneImages: "Êtes-vous certain de vouloir détruire toutes les images non utilisés ?", + ConfirmPruneContainers: "Êtes-vous certain de vouloir détruire tous les conteneurs arrêtés ?", + ConfirmStopContainers: "Êtes-vous certain de vouloir arrêter tous les conteneurs ?", + ConfirmRemoveContainers: "Êtes-vous certain de vouloir supprimer tous les conteneurs ?", + ConfirmPruneVolumes: "Êtes-vous certain de vouloir détruire tous les volumes non utilisés ?", + StopService: "Êtes-vous certain de vouloir arrêter le conteneur de ce service ?", + StopContainer: "Êtes-vous certain de vouloir arrêter ce conteneur ?", + PressEnterToReturn: "Appuiez sur Enter pour revenir à lazydocker (ce message peut être désactivé dans vos configurations en appliquant `gui.returnImmediately: true`)", + + No: "non", + Yes: "oui", + } +} diff --git a/pkg/i18n/i18n.go b/pkg/i18n/i18n.go index 25ad5a1f..658fd67d 100644 --- a/pkg/i18n/i18n.go +++ b/pkg/i18n/i18n.go @@ -53,6 +53,7 @@ func GetTranslationSets() map[string]TranslationSet { "de": germanSet(), "tr": turkishSet(), "en": englishSet(), + "fr": frenchSet(), } } From 0b8d4779764120e113d930e8c9bbf3a19e71cd32 Mon Sep 17 00:00:00 2001 From: Clement Sauvage Date: Tue, 11 Oct 2022 21:11:54 +0200 Subject: [PATCH 03/42] Remove Keybindings_fr.md --- docs/keybindings/Keybindings_fr.md | 75 ------------------------------ 1 file changed, 75 deletions(-) delete mode 100644 docs/keybindings/Keybindings_fr.md diff --git a/docs/keybindings/Keybindings_fr.md b/docs/keybindings/Keybindings_fr.md deleted file mode 100644 index 124e0b51..00000000 --- a/docs/keybindings/Keybindings_fr.md +++ /dev/null @@ -1,75 +0,0 @@ -# Menu Lazydocker - -## Projet - -
-  e: modifier la configuration lazydocker
-  o: ouvrir la configuration lazydocker
-  [: onglet précédent
-  ]: onglet suivant
-  m: voir le journal
-  enter: mise au point sur le paneau principale
-
- -## Conteneurs - -
-  [: onglet précédent
-  ]: onglet suivant
-  d: retirer
-  e: Cacher/Montrer les conteneurs arrêtés
-  s: arrêter
-  r: redémarrer
-  a: attacher
-  m: voir le journal
-  E: shell d'exécution
-  c: lancer une commande pérsonnalisée prédéfinie
-  b: voir les commandes groupés
-  w: ouvrir dans le navigateur (le premier port est http)
-  enter: mise au point sur le paneau principale
-
- -## Services - -
-  d: retirer les conteneurs
-  s: arrêter
-  r: redémarrer
-  a: attacher
-  m: voir le journal
-  [: onglet précédent
-  ]: onglet suivant
-  R: voir les options de redémarrage
-  c: lancer une commande pérsonnalisée prédéfinie
-  b: voir les commandes groupés
-  w: ouvrir dans le navigateur (le premier port est http)
-  enter: mise au point sur le paneau principale
-
- -## Images - -
-  [: onglet précédent
-  ]: onglet suivant
-  c: lancer une commande pérsonnalisée prédéfinie
-  d: retirer l'image
-  b: voir les commandes groupés
-  enter: mise au point sur le paneau principale
-
- -## Volumes - -
-  [: onglet précédent
-  ]: onglet suivant
-  c: lancer une commande pérsonnalisée prédéfinie
-  d: retirer le volume
-  b: voir les commandes groupés
-  enter: mise au point sur le paneau principale
-
- -## Principale - -
-  esc: retour
-
From 2afd84d19285777c9bcfb318fed2907a908204b6 Mon Sep 17 00:00:00 2001 From: Clement Sauvage Date: Tue, 11 Oct 2022 21:13:59 +0200 Subject: [PATCH 04/42] Generate Keybindings_fr.md --- docs/keybindings/Keybindings_fr.md | 79 ++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 docs/keybindings/Keybindings_fr.md diff --git a/docs/keybindings/Keybindings_fr.md b/docs/keybindings/Keybindings_fr.md new file mode 100644 index 00000000..895a3f14 --- /dev/null +++ b/docs/keybindings/Keybindings_fr.md @@ -0,0 +1,79 @@ +# Lazydocker menu + +## Projet + +
+  e: modifier la configuration lazydocker
+  o: ouvrire la configuration lazydocker
+  [: onglet précédent
+  ]: onglet suivant
+  m: voir les enregistrements
+  enter: focus paneau principal
+
+ +## Conteneurs + +
+  [: onglet précédent
+  ]: onglet suivant
+  d: supprimer
+  e: cacher/montrer les conteneurs arrêtés
+  p: pause
+  s: arrêter
+  r: redémarrer
+  a: attacher
+  m: voir les enregistrements
+  E: executer le shell
+  c: executer une commande prédéfinie
+  b: voir les commandes groupés
+  w: ouvrir dans le navgateur (le premier port est http)
+  enter: focus paneau principal
+
+ +## Services + +
+  d: supprimer les conteneurs
+  s: arrêter
+  p: pause
+  r: redémarrer
+  S: démarrer
+  a: attacher
+  m: voir les enregistrements
+  [: onglet précédent
+  ]: onglet suivant
+  R: voir les options de redémarrage
+  c: executer une commande prédéfinie
+  b: voir les commandes groupés
+  E: executer le shell
+  w: ouvrir dans le navgateur (le premier port est http)
+  enter: focus paneau principal
+
+ +## Images + +
+  [: onglet précédent
+  ]: onglet suivant
+  c: executer une commande prédéfinie
+  d: supprimer l'image
+  b: voir les commandes groupés
+  enter: focus paneau principal
+
+ +## Volumes + +
+  [: onglet précédent
+  ]: onglet suivant
+  c: executer une commande prédéfinie
+  d: supprimer le volume
+  b: voir les commandes groupés
+  enter: focus paneau principal
+
+ +## Principal + +
+  esc: retour
+
From 32ffbae56c3945650b5c6c2c0f0d168fcb84249b Mon Sep 17 00:00:00 2001 From: KOLOSSA Tilman Date: Tue, 16 Aug 2022 13:53:33 +0200 Subject: [PATCH 05/42] changed "white" colors to "default" to work in both light and dark terminals --- pkg/gui/confirmation_panel.go | 6 ++++-- pkg/gui/gocui.go | 2 +- pkg/gui/menu_panel.go | 2 +- pkg/utils/utils.go | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/pkg/gui/confirmation_panel.go b/pkg/gui/confirmation_panel.go index 9a8a5504..b20cbabf 100644 --- a/pkg/gui/confirmation_panel.go +++ b/pkg/gui/confirmation_panel.go @@ -81,7 +81,7 @@ func (gui *Gui) prepareConfirmationPanel(currentView *gocui.View, title, prompt gui.g.StartTicking() confirmationView.Title = title confirmationView.Wrap = true - confirmationView.FgColor = gocui.ColorWhite + confirmationView.FgColor = gocui.ColorDefault } gui.g.Update(func(g *gocui.Gui) error { return gui.switchFocus(gui.g, currentView, confirmationView, false) @@ -147,7 +147,9 @@ func (gui *Gui) setKeyBindings(g *gocui.Gui, handleConfirm, handleClose func(*go } // createSpecificErrorPanel allows you to create an error popup, specifying the -// view to be focused when the user closes the popup, and a boolean specifying +// +// 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 diff --git a/pkg/gui/gocui.go b/pkg/gui/gocui.go index d06002cf..e4c0b155 100644 --- a/pkg/gui/gocui.go +++ b/pkg/gui/gocui.go @@ -32,7 +32,7 @@ func GetGocuiAttribute(key string) gocui.Attribute { if present { return value } - return gocui.ColorWhite + return gocui.ColorDefault } // GetGocuiStyle bitwise OR's a list of attributes obtained via the given keys diff --git a/pkg/gui/menu_panel.go b/pkg/gui/menu_panel.go index 39abc108..80cb7d94 100644 --- a/pkg/gui/menu_panel.go +++ b/pkg/gui/menu_panel.go @@ -75,7 +75,7 @@ func (gui *Gui) createMenu(title string, items interface{}, itemCount int, handl x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(gui.g, false, list) menuView, _ := gui.g.SetView("menu", x0, y0, x1, y1, 0) menuView.Title = title - menuView.FgColor = gocui.ColorWhite + menuView.FgColor = gocui.ColorDefault menuView.Clear() fmt.Fprint(menuView, list) gui.State.Panels.Menu.SelectedLine = 0 diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 2d9a73bc..fe1ae3de 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -293,7 +293,7 @@ func GetGocuiAttribute(key string) gocui.Attribute { if present { return value } - return gocui.ColorWhite + return gocui.ColorDefault } // GetColorAttribute gets the color attribute from the string From 994d3c79c4234c81f81bf6572238d4dd6eddda8a Mon Sep 17 00:00:00 2001 From: KOLOSSA Tilman Date: Tue, 16 Aug 2022 13:59:24 +0200 Subject: [PATCH 06/42] fixed faulty auto-formatting --- pkg/gui/confirmation_panel.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pkg/gui/confirmation_panel.go b/pkg/gui/confirmation_panel.go index b20cbabf..a3779289 100644 --- a/pkg/gui/confirmation_panel.go +++ b/pkg/gui/confirmation_panel.go @@ -147,9 +147,7 @@ func (gui *Gui) setKeyBindings(g *gocui.Gui, handleConfirm, handleClose func(*go } // createSpecificErrorPanel allows you to create an error popup, specifying the -// -// view to be focused when the user closes the popup, and a boolean specifying -// +// 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 From f01864311c3ec69682cb67472a2a5d7d63337a1e Mon Sep 17 00:00:00 2001 From: README-bot Date: Sat, 8 Oct 2022 15:59:39 +0000 Subject: [PATCH 07/42] Updated README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ef3a2894..438a1d29 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ A simple terminal UI for both docker and docker-compose, written in Go with the

- +

## Elevator Pitch From 2bd2843d3db2a0b6a6f6fefc4582ce0225a08985 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sat, 8 Oct 2022 09:10:38 -0700 Subject: [PATCH 08/42] appease linter see if this works --- pkg/gui/confirmation_panel.go | 15 ++++----------- pkg/gui/containers_panel.go | 10 +++++----- pkg/gui/images_panel.go | 2 +- pkg/gui/layout.go | 2 +- pkg/gui/menu_panel.go | 2 +- pkg/gui/services_panel.go | 6 +++--- pkg/gui/view_helpers.go | 23 ++++++++++++++++------- pkg/gui/volumes_panel.go | 2 +- 8 files changed, 32 insertions(+), 30 deletions(-) diff --git a/pkg/gui/confirmation_panel.go b/pkg/gui/confirmation_panel.go index a3779289..e2df8438 100644 --- a/pkg/gui/confirmation_panel.go +++ b/pkg/gui/confirmation_panel.go @@ -101,6 +101,8 @@ func (gui *Gui) onNewPopupPanel() { } // 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 +// unparam complains that handleClose is alwans nil but one day it won't be nil. +// nolint:unparam func (gui *Gui) createConfirmationPanel(g *gocui.Gui, currentView *gocui.View, title, prompt string, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error { return gui.createPopupPanel(g, currentView, title, prompt, false, handleConfirm, handleClose) } @@ -146,19 +148,10 @@ func (gui *Gui) setKeyBindings(g *gocui.Gui, handleConfirm, handleClose func(*go return nil } -// 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 { +func (gui *Gui) createErrorPanel(g *gocui.Gui, message string) error { colorFunction := color.New(color.FgRed).SprintFunc() coloredMessage := colorFunction(strings.TrimSpace(message)) - return gui.createConfirmationPanel(gui.g, nextView, gui.Tr.ErrorTitle, coloredMessage, nil, nil) -} - -func (gui *Gui) createErrorPanel(g *gocui.Gui, message string) error { - return gui.createSpecificErrorPanel(message, g.CurrentView(), true) + return gui.createConfirmationPanel(gui.g, g.CurrentView(), gui.Tr.ErrorTitle, coloredMessage, nil, nil) } func (gui *Gui) renderConfirmationOptions() error { diff --git a/pkg/gui/containers_panel.go b/pkg/gui/containers_panel.go index ae8d719e..62f2119d 100644 --- a/pkg/gui/containers_panel.go +++ b/pkg/gui/containers_panel.go @@ -51,7 +51,7 @@ func (gui *Gui) handleContainerSelect(g *gocui.Gui, v *gocui.View) error { return nil } - gui.focusPoint(0, gui.State.Panels.Containers.SelectedLine, len(gui.DockerCommand.DisplayContainers), v) + gui.focusY(gui.State.Panels.Containers.SelectedLine, len(gui.DockerCommand.DisplayContainers), v) key := "containers-" + container.ID + "-" + gui.getContainerContexts()[gui.State.Panels.Containers.ContextIndex] if !gui.shouldRefresh(key) { @@ -199,7 +199,7 @@ func (gui *Gui) renderContainerStats(container *commands.Container) error { _ = gui.createErrorPanel(gui.g, err.Error()) } - _ = gui.reRenderString(gui.g, "main", contents) + gui.reRenderStringMain(contents) }) } @@ -211,10 +211,10 @@ func (gui *Gui) renderContainerTop(container *commands.Container) error { return gui.T.NewTickerTask(time.Second, func(stop chan struct{}) { gui.clearMainView() }, func(stop, notifyStopped chan struct{}) { contents, err := container.RenderTop() if err != nil { - _ = gui.reRenderString(gui.g, "main", err.Error()) + gui.reRenderStringMain(err.Error()) } - _ = gui.reRenderString(gui.g, "main", contents) + gui.reRenderStringMain(contents) }) } @@ -244,7 +244,7 @@ func (gui *Gui) refreshContainersAndServices() error { break } gui.State.Panels.Services.SelectedLine = i - gui.focusPoint(0, i, len(gui.DockerCommand.Services), gui.getServicesView()) + gui.focusY(i, len(gui.DockerCommand.Services), gui.getServicesView()) } } } diff --git a/pkg/gui/images_panel.go b/pkg/gui/images_panel.go index 0d5aba6c..da69c93f 100644 --- a/pkg/gui/images_panel.go +++ b/pkg/gui/images_panel.go @@ -50,7 +50,7 @@ func (gui *Gui) handleImageSelect(g *gocui.Gui, v *gocui.View) error { return gui.renderString(g, "main", gui.Tr.NoImages) } - gui.focusPoint(0, gui.State.Panels.Images.SelectedLine, len(gui.DockerCommand.Images), v) + gui.focusY(gui.State.Panels.Images.SelectedLine, len(gui.DockerCommand.Images), v) key := "images-" + Image.ID + "-" + gui.getImageContexts()[gui.State.Panels.Images.ContextIndex] if !gui.shouldRefresh(key) { diff --git a/pkg/gui/layout.go b/pkg/gui/layout.go index a7659754..d4765f5e 100644 --- a/pkg/gui/layout.go +++ b/pkg/gui/layout.go @@ -300,6 +300,6 @@ func (gui *Gui) focusPointInView(view *gocui.View) { } if state, ok := listViews[view.Name()]; ok { - gui.focusPoint(0, state.selectedLine, state.lineCount, view) + gui.focusY(state.selectedLine, state.lineCount, view) } } diff --git a/pkg/gui/menu_panel.go b/pkg/gui/menu_panel.go index 80cb7d94..40bb30ac 100644 --- a/pkg/gui/menu_panel.go +++ b/pkg/gui/menu_panel.go @@ -10,7 +10,7 @@ import ( // list panel functions func (gui *Gui) handleMenuSelect(g *gocui.Gui, v *gocui.View) error { - gui.focusPoint(0, gui.State.Panels.Menu.SelectedLine, gui.State.MenuItemCount, v) + gui.focusY(gui.State.Panels.Menu.SelectedLine, gui.State.MenuItemCount, v) return nil } diff --git a/pkg/gui/services_panel.go b/pkg/gui/services_panel.go index 7a9cbb44..d290911e 100644 --- a/pkg/gui/services_panel.go +++ b/pkg/gui/services_panel.go @@ -56,7 +56,7 @@ func (gui *Gui) handleServiceSelect(g *gocui.Gui, v *gocui.View) error { containerID = service.Container.ID } - gui.focusPoint(0, gui.State.Panels.Services.SelectedLine, len(gui.DockerCommand.Services), v) + gui.focusY(gui.State.Panels.Services.SelectedLine, len(gui.DockerCommand.Services), v) key := "services-" + service.ID + "-" + containerID + "-" + gui.getServiceContexts()[gui.State.Panels.Services.ContextIndex] if !gui.shouldRefresh(key) { @@ -118,10 +118,10 @@ func (gui *Gui) renderServiceTop(service *commands.Service) error { return gui.T.NewTickerTask(time.Second, func(stop chan struct{}) { gui.clearMainView() }, func(stop, notifyStopped chan struct{}) { contents, err := service.RenderTop() if err != nil { - _ = gui.reRenderString(gui.g, "main", err.Error()) + gui.reRenderStringMain(err.Error()) } - _ = gui.reRenderString(gui.g, "main", contents) + gui.reRenderStringMain(contents) }) } diff --git a/pkg/gui/view_helpers.go b/pkg/gui/view_helpers.go index 08033e79..a08b3a73 100644 --- a/pkg/gui/view_helpers.go +++ b/pkg/gui/view_helpers.go @@ -156,6 +156,7 @@ func (gui *Gui) switchFocus(g *gocui.Gui, oldView, newView *gocui.View, returnin } // if the cursor down past the last item, move it to the last line +// nolint:unparam func (gui *Gui) focusPoint(selectedX int, selectedY int, lineCount int, v *gocui.View) { if selectedY < 0 || selectedY > lineCount { return @@ -192,12 +193,16 @@ func (gui *Gui) focusPoint(selectedX int, selectedY int, lineCount int, v *gocui } } +func (gui *Gui) focusY(selectedY int, lineCount int, v *gocui.View) { + gui.focusPoint(0, selectedY, lineCount, v) +} + func (gui *Gui) cleanString(s string) string { output := string(bom.Clean([]byte(s))) return utils.NormalizeLinefeeds(output) } -func (gui *Gui) setViewContent(g *gocui.Gui, v *gocui.View, s string) error { +func (gui *Gui) setViewContent(v *gocui.View, s string) error { v.Clear() fmt.Fprint(v, gui.cleanString(s)) return nil @@ -216,21 +221,25 @@ func (gui *Gui) renderString(g *gocui.Gui, viewName, s string) error { if err := v.SetCursor(0, 0); err != nil { return err } - return gui.setViewContent(gui.g, v, s) + return gui.setViewContent(v, s) }) return nil } +// reRenderString sets the main view's content, without changing its origin +func (gui *Gui) reRenderStringMain(s string) { + gui.reRenderString("main", s) +} + // reRenderString sets the view's content, without changing its origin -func (gui *Gui) reRenderString(g *gocui.Gui, viewName, s string) error { - g.Update(func(*gocui.Gui) error { - v, err := g.View(viewName) +func (gui *Gui) reRenderString(viewName, s string) { + gui.g.Update(func(*gocui.Gui) error { + v, err := gui.g.View(viewName) if err != nil { return nil // return gracefully if view has been deleted } - return gui.setViewContent(gui.g, v, s) + return gui.setViewContent(v, s) }) - return nil } func (gui *Gui) optionsMapToString(optionsMap map[string]string) string { diff --git a/pkg/gui/volumes_panel.go b/pkg/gui/volumes_panel.go index 0ea4f735..6d69e95a 100644 --- a/pkg/gui/volumes_panel.go +++ b/pkg/gui/volumes_panel.go @@ -47,7 +47,7 @@ func (gui *Gui) handleVolumeSelect(g *gocui.Gui, v *gocui.View) error { return gui.renderString(g, "main", gui.Tr.NoVolumes) } - gui.focusPoint(0, gui.State.Panels.Volumes.SelectedLine, len(gui.DockerCommand.Volumes), v) + gui.focusY(gui.State.Panels.Volumes.SelectedLine, len(gui.DockerCommand.Volumes), v) key := "volumes-" + volume.Name + "-" + gui.getVolumeContexts()[gui.State.Panels.Volumes.ContextIndex] if !gui.shouldRefresh(key) { From 63fd1da1a26d70f36e3bf603057b89ebce2088d3 Mon Sep 17 00:00:00 2001 From: blennster Date: Wed, 21 Sep 2022 00:33:28 +0200 Subject: [PATCH 09/42] add tab and backtab navigation --- pkg/gui/keybindings.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index 81e6939a..9a25f3dd 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -515,6 +515,8 @@ func (gui *Gui) GetInitialKeybindings() []*Binding { {ViewName: viewName, Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, Handler: gui.nextView}, {ViewName: viewName, Key: 'h', Modifier: gocui.ModNone, Handler: gui.previousView}, {ViewName: viewName, Key: 'l', Modifier: gocui.ModNone, Handler: gui.nextView}, + {ViewName: viewName, Key: gocui.KeyTab, Modifier: gocui.ModNone, Handler: gui.nextView}, + {ViewName: viewName, Key: gocui.KeyBacktab, Modifier: gocui.ModNone, Handler: gui.previousView}, }...) } From 4288ea05a5ecc63cb047ef334211e82a36ebb04b Mon Sep 17 00:00:00 2001 From: Miles Liu Date: Tue, 6 Sep 2022 13:39:08 +0800 Subject: [PATCH 10/42] allow user to replace docker repo prefix from image names Example usage: ```yaml replacements: imageNamePrefixes: '123456789012.dkr.ecr.us-east-1.amazonaws.com': '' '923456789999.dkr.ecr.us-east-1.amazonaws.com': '' ``` Closes #316 --- docs/Config.md | 11 +++++++++++ pkg/commands/image.go | 7 +++++++ pkg/config/app_config.go | 12 ++++++++++++ 3 files changed, 30 insertions(+) diff --git a/docs/Config.md b/docs/Config.md index f2388d5f..92a8a3ed 100644 --- a/docs/Config.md +++ b/docs/Config.md @@ -90,3 +90,14 @@ customCommands: command: 'docker exec -it {{ .Container.ID }} bash' serviceNames: [] ``` + +## Replacements + +You can add replacements like so: + +```yaml +replacements: + imageNamePrefixes: + '123456789012.dkr.ecr.us-east-1.amazonaws.com': '' + '923456789999.dkr.ecr.us-east-1.amazonaws.com': '' +``` diff --git a/pkg/commands/image.go b/pkg/commands/image.go index 796a6404..7cbb142e 100644 --- a/pkg/commands/image.go +++ b/pkg/commands/image.go @@ -123,6 +123,13 @@ func (c *DockerCommand) RefreshImages() ([]*Image, error) { if len(nameParts) > 1 { tag = nameParts[len(nameParts)-1] name = strings.Join(nameParts[:len(nameParts)-1], ":") + + for prefix, replacement := range c.Config.UserConfig.Replacements.ImageNamePrefixes { + if strings.HasPrefix(name, prefix) { + name = strings.Replace(name, prefix, replacement, 1) + break + } + } } ownImages[i] = &Image{ diff --git a/pkg/config/app_config.go b/pkg/config/app_config.go index add2ad7d..d3b3404b 100644 --- a/pkg/config/app_config.go +++ b/pkg/config/app_config.go @@ -57,6 +57,9 @@ type UserConfig struct { // Stats determines how long lazydocker will gather container stats for, and // what stat info to graph Stats StatsConfig `yaml:"stats,omitempty"` + + // Replacements determines how we render a container's info + Replacements Replacements `yaml:"replacements,omitempty"` } // ThemeConfig is for setting the colors of panels and some text. @@ -258,6 +261,12 @@ type CustomCommands struct { Volumes []CustomCommand `yaml:"volumes,omitempty"` } +// Replacements contains the stuff relating to rendering a container's info +type Replacements struct { + // ImageNamePrefixes tells us how to replace a prefix in the Docker image name + ImageNamePrefixes map[string]string `yaml:"imageNamePrefixes,omitempty"` +} + // CustomCommand is a template for a command we want to run against a service or // container type CustomCommand struct { @@ -404,6 +413,9 @@ func GetDefaultConfig() UserConfig { }, }, }, + Replacements: Replacements{ + ImageNamePrefixes: map[string]string{}, + }, } } From 9d0dbb7c431edad63dd56d8113fe3dd7f15d35c7 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sat, 8 Oct 2022 11:57:01 -0700 Subject: [PATCH 11/42] Update pkg/config/app_config.go --- pkg/config/app_config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/config/app_config.go b/pkg/config/app_config.go index d3b3404b..c087675e 100644 --- a/pkg/config/app_config.go +++ b/pkg/config/app_config.go @@ -58,7 +58,7 @@ type UserConfig struct { // what stat info to graph Stats StatsConfig `yaml:"stats,omitempty"` - // Replacements determines how we render a container's info + // Replacements determines how we render an item's info Replacements Replacements `yaml:"replacements,omitempty"` } From 6f6b0773a46afee83e1bfe0c47be0e2e42d9d578 Mon Sep 17 00:00:00 2001 From: rmt/src <144435+rmtsrc@users.noreply.github.com> Date: Fri, 3 Jun 2022 17:13:59 +0100 Subject: [PATCH 12/42] Include image name on config tab --- pkg/gui/containers_panel.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/gui/containers_panel.go b/pkg/gui/containers_panel.go index 62f2119d..8978b99e 100644 --- a/pkg/gui/containers_panel.go +++ b/pkg/gui/containers_panel.go @@ -145,6 +145,7 @@ func (gui *Gui) renderContainerConfig(container *commands.Container) error { output := "" output += utils.WithPadding("ID: ", padding) + container.ID + "\n" output += utils.WithPadding("Name: ", padding) + container.Name + "\n" + output += utils.WithPadding("Image: ", padding) + container.Details.Config.Image + "\n" output += utils.WithPadding("Command: ", padding) + strings.Join(append([]string{container.Details.Path}, container.Details.Args...), " ") + "\n" output += utils.WithPadding("Labels: ", padding) + utils.FormatMap(padding, container.Details.Config.Labels) output += "\n" From 1a9c10dc86acb62d20edf2356e91ad30c6f52908 Mon Sep 17 00:00:00 2001 From: Samuel Cattini-Schultz Date: Tue, 28 Jun 2022 23:00:21 +1000 Subject: [PATCH 13/42] Add Compose V2 instructions to FAQ --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 438a1d29..8596f5ae 100644 --- a/README.md +++ b/README.md @@ -270,6 +270,12 @@ By default we only show logs from the last hour, so that we're not putting too m If you are running lazydocker in Docker container, it is a know bug, that you can't see logs or CPU usage. +### Why isn't my docker-compose environment being used? + +By default Compose V1 (`docker-compose` with the hyphen) is used as the docker-compose command. You will need to make sure you have the `docker-compose` command available for lazydocker to be able to use. + +If you use Compose V2 (`docker compose` without the hyphen), alternatively, you can change the docker-compose command used via the `commandTemplates.dockerCompose` config value. + ## Alternatives - [docui](https://github.com/skanehira/docui) - Skanehira beat me to the punch on making a docker terminal UI, so definitely check out that repo as well! I think the two repos can live in harmony though: lazydocker is more about managing existing containers/services, and docui is more about creating and configuring them. From 982a6cbf15c71793720eb64160b2db419dc60a32 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sun, 9 Oct 2022 10:18:20 -0700 Subject: [PATCH 14/42] allow changing window size --- go.mod | 9 +- go.sum | 17 +- pkg/config/app_config.go | 21 +- pkg/gui/arrangement.go | 237 ++++ pkg/gui/gui.go | 17 + pkg/gui/keybindings.go | 18 + pkg/gui/layout.go | 239 +--- pkg/gui/theme.go | 4 +- pkg/gui/view_helpers.go | 52 + pkg/gui/views.go | 128 ++ pkg/gui/window.go | 7 + pkg/i18n/english.go | 6 + scripts/bump_gocui.sh | 5 + scripts/bump_lazycore.sh | 5 + .../github.com/jesseduffield/lazycore/LICENSE | 21 + .../lazycore/pkg/boxlayout/boxlayout.go | 211 +++ .../jesseduffield/lazycore/pkg/utils/utils.go | 16 + vendor/github.com/samber/lo/CHANGELOG.md | 124 +- vendor/github.com/samber/lo/Makefile | 11 +- vendor/github.com/samber/lo/README.md | 1196 ++++++++++++++--- vendor/github.com/samber/lo/channel.go | 228 ++++ vendor/github.com/samber/lo/condition.go | 22 + .../github.com/samber/lo/docker-compose.yml | 4 +- vendor/github.com/samber/lo/errors.go | 236 +++- vendor/github.com/samber/lo/find.go | 158 ++- vendor/github.com/samber/lo/func.go | 8 + vendor/github.com/samber/lo/intersect.go | 25 + vendor/github.com/samber/lo/map.go | 42 +- vendor/github.com/samber/lo/math.go | 7 +- vendor/github.com/samber/lo/pointers.go | 32 - vendor/github.com/samber/lo/retry.go | 9 +- vendor/github.com/samber/lo/slice.go | 229 +++- vendor/github.com/samber/lo/string.go | 37 +- vendor/github.com/samber/lo/test.go | 32 + vendor/github.com/samber/lo/tuples.go | 136 +- .../github.com/samber/lo/type_manipulation.go | 88 ++ .../testify/assert/assertion_compare.go | 76 +- .../assert/assertion_compare_can_convert.go | 16 + .../assert/assertion_compare_legacy.go | 16 + .../testify/assert/assertion_format.go | 22 + .../testify/assert/assertion_forward.go | 44 + .../testify/assert/assertion_order.go | 8 +- .../stretchr/testify/assert/assertions.go | 190 ++- vendor/gopkg.in/yaml.v3/decode.go | 78 +- vendor/gopkg.in/yaml.v3/parserc.go | 11 +- vendor/modules.txt | 10 +- 46 files changed, 3452 insertions(+), 656 deletions(-) create mode 100644 pkg/gui/arrangement.go create mode 100644 pkg/gui/views.go create mode 100644 pkg/gui/window.go create mode 100755 scripts/bump_gocui.sh create mode 100755 scripts/bump_lazycore.sh create mode 100644 vendor/github.com/jesseduffield/lazycore/LICENSE create mode 100644 vendor/github.com/jesseduffield/lazycore/pkg/boxlayout/boxlayout.go create mode 100644 vendor/github.com/jesseduffield/lazycore/pkg/utils/utils.go create mode 100644 vendor/github.com/samber/lo/channel.go create mode 100644 vendor/github.com/samber/lo/func.go delete mode 100644 vendor/github.com/samber/lo/pointers.go create mode 100644 vendor/github.com/samber/lo/test.go create mode 100644 vendor/github.com/samber/lo/type_manipulation.go create mode 100644 vendor/github.com/stretchr/testify/assert/assertion_compare_can_convert.go create mode 100644 vendor/github.com/stretchr/testify/assert/assertion_compare_legacy.go diff --git a/go.mod b/go.mod index c7925e43..b8a9dcd7 100644 --- a/go.mod +++ b/go.mod @@ -16,13 +16,15 @@ require ( github.com/jesseduffield/asciigraph v0.0.0-20190605104717-6d88e39309ee github.com/jesseduffield/gocui v0.3.1-0.20220417002912-bce22fd599f6 github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10 + github.com/jesseduffield/lazycore v0.0.0-20221009164442-17c8b878c316 github.com/jesseduffield/yaml v0.0.0-20190702115811-b900b7e08b56 + github.com/mattn/go-runewidth v0.0.13 github.com/mcuadros/go-lookup v0.0.0-20171110082742-5650f26be767 github.com/mgutz/str v1.2.0 - github.com/samber/lo v1.20.0 + github.com/samber/lo v1.31.0 github.com/sirupsen/logrus v1.4.2 github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad - github.com/stretchr/testify v1.7.0 + github.com/stretchr/testify v1.8.0 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 ) @@ -39,7 +41,6 @@ require ( github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-colorable v0.1.4 // indirect github.com/mattn/go-isatty v0.0.11 // indirect - github.com/mattn/go-runewidth v0.0.13 // indirect github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c // indirect github.com/onsi/ginkgo v1.8.0 // indirect @@ -57,6 +58,6 @@ require ( golang.org/x/text v0.3.7 // indirect golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect gopkg.in/yaml.v2 v2.2.2 // indirect - gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect gotest.tools/v3 v3.2.0 // indirect ) diff --git a/go.sum b/go.sum index b72e4b19..7daa24f7 100644 --- a/go.sum +++ b/go.sum @@ -54,6 +54,8 @@ github.com/jesseduffield/gocui v0.3.1-0.20220417002912-bce22fd599f6 h1:Fmay0Lz21 github.com/jesseduffield/gocui v0.3.1-0.20220417002912-bce22fd599f6/go.mod h1:znJuCDnF2Ph40YZSlBwdX/4GEofnIoWLGdT4mK5zRAU= github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10 h1:jmpr7KpX2+2GRiE91zTgfq49QvgiqB0nbmlwZ8UnOx0= github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10/go.mod h1:aA97kHeNA+sj2Hbki0pvLslmE4CbDyhBeSSTUUnOuVo= +github.com/jesseduffield/lazycore v0.0.0-20221009164442-17c8b878c316 h1:UzTg0utG+cLS94Qjb/ZFDzfMeZDFXErSbNgJOyj70EA= +github.com/jesseduffield/lazycore v0.0.0-20221009164442-17c8b878c316/go.mod h1:qxN4mHOAyeIDLP7IK7defgPClM/z1Kze8VVQiaEjzsQ= github.com/jesseduffield/yaml v0.0.0-20190702115811-b900b7e08b56 h1:33wSxJWU/f2TAozHYtJ8zqBxEnEVYM+22moLoiAkxvg= github.com/jesseduffield/yaml v0.0.0-20190702115811-b900b7e08b56/go.mod h1:FZJBwOhE+RXz8EVZfY+xnbCw2cVOwxlK3/aIi581z/s= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= @@ -99,8 +101,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/samber/lo v1.20.0 h1:20FtphdORvp4yxklurzZv2HX+g+0urEMQziODC5bV70= -github.com/samber/lo v1.20.0/go.mod h1:2I7tgIv8Q1SG2xEIkRq0F2i2zgxVpnyPOP0d3Gj2r+A= +github.com/samber/lo v1.31.0 h1:Sfa+/064Tdo4SvlohQUQzBhgSer9v/coGvKQI/XLWAM= +github.com/samber/lo v1.31.0/go.mod h1:HLeWcJRRyLKp3+/XBJvOrerCQn9mhdKMHyd7IRlgeQ8= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= @@ -108,11 +110,14 @@ github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad h1:fiWzISvDn0Csy5H0iwgAuJGQTUpVfEMJJd4nRFXogbc= github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M= github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8= github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= @@ -177,8 +182,8 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= gotest.tools/v3 v3.2.0 h1:I0DwBVMGAx26dttAj1BtJLAkVGncrkkUXfJLC4Flt/I= gotest.tools/v3 v3.2.0/go.mod h1:Mcr9QNxkg0uMvy/YElmo4SpXgJKWgQvYrT7Kw5RzJ1A= diff --git a/pkg/config/app_config.go b/pkg/config/app_config.go index c087675e..9094104f 100644 --- a/pkg/config/app_config.go +++ b/pkg/config/app_config.go @@ -114,6 +114,16 @@ type GuiConfig struct { // By default, containers are now sorted by status. This setting allows users to // use legacy behaviour instead. LegacySortContainers bool `yaml:"legacySortContainers,omitempty"` + + // If 0.333, then the side panels will be 1/3 of the screen's width + SidePanelWidth float64 `yaml:"sidePanelWidth"` + + // Determines whether we show the bottom line (the one containing keybinding + // info and the status of the app). + ShowBottomLine bool `yaml:"showBottomLine"` + + // When true, ensures that the focused side panel always takes up the full vertical space + ExpandFocusedSidePanel bool `yaml:"expandFocusedSidePanel"` } // CommandTemplatesConfig determines what commands actually get called when we @@ -320,10 +330,13 @@ func GetDefaultConfig() UserConfig { InactiveBorderColor: []string{"default"}, OptionsTextColor: []string{"blue"}, }, - ShowAllContainers: false, - ReturnImmediately: false, - WrapMainPanel: true, - LegacySortContainers: false, + ShowAllContainers: false, + ReturnImmediately: false, + WrapMainPanel: true, + LegacySortContainers: false, + SidePanelWidth: 0.3333, + ShowBottomLine: true, + ExpandFocusedSidePanel: false, }, ConfirmOnQuit: false, Logs: LogsConfig{ diff --git a/pkg/gui/arrangement.go b/pkg/gui/arrangement.go new file mode 100644 index 00000000..58b53706 --- /dev/null +++ b/pkg/gui/arrangement.go @@ -0,0 +1,237 @@ +package gui + +import ( + "github.com/jesseduffield/lazycore/pkg/boxlayout" + "github.com/jesseduffield/lazydocker/pkg/utils" + "github.com/mattn/go-runewidth" + "github.com/samber/lo" +) + +// In this file we use the boxlayout package, along with knowledge about the app's state, +// to arrange the windows (i.e. panels) on the screen. + +const INFO_SECTION_PADDING = " " + +func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map[string]boxlayout.Dimensions { + minimumHeight := 9 + minimumWidth := 10 + width, height := gui.g.Size() + if width < minimumWidth || height < minimumHeight { + return boxlayout.ArrangeWindows(&boxlayout.Box{Window: "limit"}, 0, 0, width, height) + } + + sideSectionWeight, mainSectionWeight := gui.getMidSectionWeights() + + sidePanelsDirection := boxlayout.COLUMN + portraitMode := width <= 84 && height > 45 + if portraitMode { + sidePanelsDirection = boxlayout.ROW + } + + showInfoSection := gui.Config.UserConfig.Gui.ShowBottomLine + infoSectionSize := 0 + if showInfoSection { + infoSectionSize = 1 + } + + root := &boxlayout.Box{ + Direction: boxlayout.ROW, + Children: []*boxlayout.Box{ + { + Direction: sidePanelsDirection, + Weight: 1, + Children: []*boxlayout.Box{ + { + Direction: boxlayout.ROW, + Weight: sideSectionWeight, + ConditionalChildren: gui.sidePanelChildren, + }, + { + Window: "main", + Weight: mainSectionWeight, + }, + }, + }, + { + Direction: boxlayout.COLUMN, + Size: infoSectionSize, + Children: gui.infoSectionChildren(informationStr, appStatus), + }, + }, + } + + return boxlayout.ArrangeWindows(root, 0, 0, width, height) +} + +func MergeMaps[K comparable, V any](maps ...map[K]V) map[K]V { + result := map[K]V{} + for _, currMap := range maps { + for key, value := range currMap { + result[key] = value + } + } + + return result +} + +func (gui *Gui) getMidSectionWeights() (int, int) { + currentWindow := gui.currentWindow() + + // we originally specified this as a ratio i.e. .20 would correspond to a weight of 1 against 4 + sidePanelWidthRatio := gui.Config.UserConfig.Gui.SidePanelWidth + // we could make this better by creating ratios like 2:3 rather than always 1:something + mainSectionWeight := int(1/sidePanelWidthRatio) - 1 + sideSectionWeight := 1 + + if currentWindow == "main" && gui.State.ScreenMode == SCREEN_FULL { + mainSectionWeight = 1 + sideSectionWeight = 0 + } else { + if gui.State.ScreenMode == SCREEN_HALF { + mainSectionWeight = 1 + } else if gui.State.ScreenMode == SCREEN_FULL { + mainSectionWeight = 0 + } + } + + return sideSectionWeight, mainSectionWeight +} + +func (gui *Gui) infoSectionChildren(informationStr string, appStatus string) []*boxlayout.Box { + result := []*boxlayout.Box{} + + if len(appStatus) > 0 { + result = append(result, + &boxlayout.Box{ + Window: "appStatus", + Size: runewidth.StringWidth(appStatus) + runewidth.StringWidth(INFO_SECTION_PADDING), + }, + ) + } + + result = append(result, + []*boxlayout.Box{ + { + Window: "options", + Weight: 1, + }, + { + Window: "information", + // unlike appStatus, informationStr has various colors so we need to decolorise before taking the length + Size: runewidth.StringWidth(INFO_SECTION_PADDING) + runewidth.StringWidth(utils.Decolorise(informationStr)), + }, + }..., + ) + + return result +} + +func (gui *Gui) sideViewNames() []string { + if gui.DockerCommand.InDockerComposeProject { + return []string{"project", "services", "containers", "images", "volumes"} + } else { + return []string{"project", "containers", "images", "volumes"} + } +} + +func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box { + currentWindow := gui.currentSideWindowName() + sideWindowNames := gui.sideViewNames() + + if gui.State.ScreenMode == SCREEN_FULL || gui.State.ScreenMode == SCREEN_HALF { + fullHeightBox := func(window string) *boxlayout.Box { + if window == currentWindow { + return &boxlayout.Box{ + Window: window, + Weight: 1, + } + } else { + return &boxlayout.Box{ + Window: window, + Size: 0, + } + } + } + + return lo.Map(sideWindowNames, func(window string, _ int) *boxlayout.Box { + return fullHeightBox(window) + }) + + } else if height >= 28 { + accordionMode := gui.Config.UserConfig.Gui.ExpandFocusedSidePanel + accordionBox := func(defaultBox *boxlayout.Box) *boxlayout.Box { + if accordionMode && defaultBox.Window == currentWindow { + return &boxlayout.Box{ + Window: defaultBox.Window, + Weight: 2, + } + } + + return defaultBox + } + + return append([]*boxlayout.Box{ + { + Window: sideWindowNames[0], + Size: 3, + }, + }, lo.Map(sideWindowNames[1:], func(window string, _ int) *boxlayout.Box { + return accordionBox(&boxlayout.Box{Window: window, Weight: 1}) + })...) + } else { + squashedHeight := 1 + if height >= 21 { + squashedHeight = 3 + } + + squashedSidePanelBox := func(window string) *boxlayout.Box { + if window == currentWindow { + return &boxlayout.Box{ + Window: window, + Weight: 1, + } + } else { + return &boxlayout.Box{ + Window: window, + Size: squashedHeight, + } + } + } + + return lo.Map(sideWindowNames, func(window string, _ int) *boxlayout.Box { + return squashedSidePanelBox(window) + }) + } +} + +// TODO: reintroduce +// func (gui *Gui) currentSideWindowName() string { +// // there is always one and only one cyclable context in the context stack. We'll look from top to bottom +// gui.State.ContextManager.RLock() +// defer gui.State.ContextManager.RUnlock() + +// for idx := range gui.State.ContextManager.ContextStack { +// reversedIdx := len(gui.State.ContextManager.ContextStack) - 1 - idx +// context := gui.State.ContextManager.ContextStack[reversedIdx] + +// if context.GetKind() == types.SIDE_CONTEXT { +// return context.GetWindowName() +// } +// } + +// return "files" // default +// } + +func (gui *Gui) currentNonPopupWindowName() string { + return gui.peekPreviousView() +} + +// TODO: do this better. +func (gui *Gui) currentSideWindowName() string { + windowName := gui.currentWindow() + if !lo.Contains(gui.sideViewNames(), windowName) { + return gui.peekPreviousView() + } + + return windowName +} diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 2818e565..ccafc9ef 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -73,6 +73,9 @@ type Gui struct { T *tasks.TaskManager ErrorChan chan error CyclableViews []string + Views Views + + ViewsSetup bool } type servicePanelState struct { @@ -131,8 +134,22 @@ type guiState struct { // We increment it each time we switch to a new subprocess // Every time we go to a subprocess we need to close a few goroutines so this index is used for that purpose SessionIndex int + + ScreenMode WindowMaximisation } +// screen sizing determines how much space your selected window takes up (window +// as in panel, not your terminal's window). Sometimes you want a bit more space +// to see the contents of a panel, and this keeps track of how much maximisation +// you've set +type WindowMaximisation int + +const ( + SCREEN_NORMAL WindowMaximisation = iota + SCREEN_HALF + SCREEN_FULL +) + // NewGui builds a new gui handler func NewGui(log *logrus.Entry, dockerCommand *commands.DockerCommand, oSCommand *commands.OSCommand, tr *i18n.TranslationSet, config *config.AppConfig, errorChan chan error) (*Gui, error) { initialState := guiState{ diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index 9a25f3dd..05ce3498 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -506,6 +506,18 @@ func (gui *Gui) GetInitialKeybindings() []*Binding { Modifier: gocui.ModNone, Handler: gui.scrollRightMain, }, + { + ViewName: "", + Key: '+', + Handler: wrappedHandler(gui.nextScreenMode), + Description: gui.Tr.LcNextScreenMode, + }, + { + ViewName: "", + Key: '_', + Handler: wrappedHandler(gui.prevScreenMode), + Description: gui.Tr.LcPrevScreenMode, + }, } // TODO: add more views here @@ -573,3 +585,9 @@ func (gui *Gui) keybindings(g *gocui.Gui) error { return nil } + +func wrappedHandler(f func() error) func(*gocui.Gui, *gocui.View) error { + return func(g *gocui.Gui, v *gocui.View) error { + return f() + } +} diff --git a/pkg/gui/layout.go b/pkg/gui/layout.go index d4765f5e..22f5fd89 100644 --- a/pkg/gui/layout.go +++ b/pkg/gui/layout.go @@ -1,11 +1,11 @@ package gui import ( - "github.com/fatih/color" "github.com/jesseduffield/gocui" - "github.com/jesseduffield/lazydocker/pkg/utils" ) +const UNKNOWN_VIEW_ERROR_MSG = "unknown view" + // getFocusLayout returns a manager function for when view gain and lose focus func (gui *Gui) getFocusLayout() func(g *gocui.Gui) error { var previousView *gocui.View @@ -59,204 +59,61 @@ func (gui *Gui) onFocus(v *gocui.View) { // layout is called for every screen re-render e.g. when the screen is resized func (gui *Gui) layout(g *gocui.Gui) error { + if !gui.ViewsSetup { + if err := gui.createAllViews(); err != nil { + return err + } + + gui.ViewsSetup = true + } + g.Highlight = true width, height := g.Size() - information := gui.Config.Version - if gui.g.Mouse { - donate := color.New(color.FgMagenta, color.Underline).Sprint(gui.Tr.Donate) - information = donate + " " + information - } - - minimumHeight := 9 - minimumWidth := 10 - if height < minimumHeight || width < minimumWidth { - v, err := g.SetView("limit", 0, 0, width-1, height-1, 0) - if err != nil { - if err.Error() != "unknown view" { - return err - } - v.Title = gui.Tr.NotEnoughSpace - v.Wrap = true - _, _ = g.SetViewOnTop("limit") - } - return nil - } - - currView := gui.g.CurrentView() - currentCyclebleView := gui.peekPreviousView() - if currView != nil { - viewName := currView.Name() - usePreviouseView := true - for _, view := range gui.CyclableViews { - if view == viewName { - currentCyclebleView = viewName - usePreviouseView = false - break - } - } - if usePreviouseView { - currentCyclebleView = gui.peekPreviousView() - } - } - - usableSpace := height - 4 - - tallPanels := 3 - var vHeights map[string]int - if gui.DockerCommand.InDockerComposeProject { - tallPanels++ - vHeights = map[string]int{ - "project": 3, - "services": usableSpace/tallPanels + usableSpace%tallPanels, - "containers": usableSpace / tallPanels, - "images": usableSpace / tallPanels, - "volumes": usableSpace / tallPanels, - "options": 1, - } - } else { - vHeights = map[string]int{ - "project": 3, - "containers": usableSpace/tallPanels + usableSpace%tallPanels, - "images": usableSpace / tallPanels, - "volumes": usableSpace / tallPanels, - "options": 1, - } - } - - if height < 28 { - defaultHeight := 3 - if height < 21 { - defaultHeight = 1 - } - vHeights = map[string]int{ - "project": defaultHeight, - "containers": defaultHeight, - "images": defaultHeight, - "volumes": defaultHeight, - "options": defaultHeight, - } - if gui.DockerCommand.InDockerComposeProject { - vHeights["services"] = defaultHeight - } - vHeights[currentCyclebleView] = height - defaultHeight*tallPanels - 1 - } - - optionsVersionBoundary := width - max(len(utils.Decolorise(information)), 1) - leftSideWidth := width / 3 - appStatus := gui.statusManager.getStatusString() - appStatusOptionsBoundary := 0 - if appStatus != "" { - appStatusOptionsBoundary = len(appStatus) + 2 - } - _, _ = g.SetViewOnBottom("limit") - _ = g.DeleteView("limit") + viewDimensions := gui.getWindowDimensions(gui.getInformationContent(), appStatus) + // we assume that the view has already been created. + setViewFromDimensions := func(viewName string, windowName string) (*gocui.View, error) { + dimensionsObj, ok := viewDimensions[windowName] - v, err := g.SetView("main", leftSideWidth+1, 0, width-1, height-2, gocui.LEFT) - if err != nil { - if err.Error() != "unknown view" { - return err - } - v.Wrap = gui.Config.UserConfig.Gui.WrapMainPanel - v.FgColor = gocui.ColorDefault - - // when you run a docker container with the -it flags (interactive mode) it adds carriage returns for some reason. This is not docker's fault, it's an os-level default. - v.IgnoreCarriageReturns = true - } - - if v, err := g.SetView("project", 0, 0, leftSideWidth, vHeights["project"]-1, gocui.BOTTOM|gocui.RIGHT); err != nil { - if err.Error() != "unknown view" { - return err - } - v.Title = gui.Tr.ProjectTitle - v.FgColor = gocui.ColorDefault - } - - var servicesView *gocui.View - aboveContainersView := "project" - if gui.DockerCommand.InDockerComposeProject { - aboveContainersView = "services" - servicesView, err = g.SetViewBeneath("services", "project", vHeights["services"]) + view, err := g.View(viewName) if err != nil { - if err.Error() != "unknown view" { - return err - } - servicesView.Highlight = true - servicesView.Title = gui.Tr.ServicesTitle - servicesView.FgColor = gocui.ColorDefault + return nil, err } + + if !ok { + // view not specified in dimensions object: so create the view and hide it + // making the view take up the whole space in the background in case it needs + // to render content as soon as it appears, because lazyloaded content (via a pty task) + // cares about the size of the view. + _, err := g.SetView(viewName, 0, 0, width, height, 0) + view.Visible = false + return view, err + } + + frameOffset := 1 + if view.Frame { + frameOffset = 0 + } + _, err = g.SetView( + viewName, + dimensionsObj.X0-frameOffset, + dimensionsObj.Y0-frameOffset, + dimensionsObj.X1+frameOffset, + dimensionsObj.Y1+frameOffset, + 0, + ) + view.Visible = true + + return view, err } - containersView, err := g.SetViewBeneath("containers", aboveContainersView, vHeights["containers"]) - if err != nil { - if err.Error() != "unknown view" { + for _, viewName := range []string{"project", "services", "containers", "images", "volumes", "options", "information", "appStatus", "main", "limit"} { + _, err := setViewFromDimensions(viewName, viewName) + if err != nil && err.Error() != UNKNOWN_VIEW_ERROR_MSG { return err } - containersView.Highlight = true - if gui.Config.UserConfig.Gui.ShowAllContainers || !gui.DockerCommand.InDockerComposeProject { - containersView.Title = gui.Tr.ContainersTitle - } else { - containersView.Title = gui.Tr.StandaloneContainersTitle - } - containersView.FgColor = gocui.ColorDefault - } - - imagesView, err := g.SetViewBeneath("images", "containers", vHeights["images"]) - if err != nil { - if err.Error() != "unknown view" { - return err - } - imagesView.Highlight = true - imagesView.Title = gui.Tr.ImagesTitle - imagesView.FgColor = gocui.ColorDefault - } - - volumesView, err := g.SetViewBeneath("volumes", "images", vHeights["volumes"]) - if err != nil { - if err.Error() != "unknown view" { - return err - } - volumesView.Highlight = true - volumesView.Title = gui.Tr.VolumesTitle - volumesView.FgColor = gocui.ColorDefault - } - - if v, err := g.SetView("options", appStatusOptionsBoundary-1, height-2, optionsVersionBoundary-1, height, 0); err != nil { - if err.Error() != "unknown view" { - return err - } - v.Frame = false - if v.FgColor, err = gui.GetOptionsPanelTextColor(); err != nil { - return err - } - } - - if appStatusView, err := g.SetView("appStatus", -1, height-2, width, height, 0); err != nil { - if err.Error() != "unknown view" { - return err - } - appStatusView.BgColor = gocui.ColorDefault - appStatusView.FgColor = gocui.ColorCyan - appStatusView.Frame = false - if _, err := g.SetViewOnBottom("appStatus"); err != nil { - return err - } - } - - if v, err := g.SetView("information", optionsVersionBoundary-1, height-2, width, height, 0); err != nil { - if err.Error() != "unknown view" { - return err - } - v.BgColor = gocui.ColorDefault - v.FgColor = gocui.ColorGreen - v.Frame = false - if err := gui.renderString(g, "information", information); err != nil { - return err - } - - gui.waitForIntro.Done() } if gui.g.CurrentView() == nil { @@ -303,3 +160,9 @@ func (gui *Gui) focusPointInView(view *gocui.View) { gui.focusY(state.selectedLine, state.lineCount, view) } } + +func (gui *Gui) prepareView(viewName string) (*gocui.View, error) { + // arbitrarily giving the view enough size so that we don't get an error, but + // it's expected that the view will be given the correct size before being shown + return gui.g.SetView(viewName, 0, 0, 10, 10, 0) +} diff --git a/pkg/gui/theme.go b/pkg/gui/theme.go index b9b28bcf..0fb740da 100644 --- a/pkg/gui/theme.go +++ b/pkg/gui/theme.go @@ -5,8 +5,8 @@ import ( ) // GetOptionsPanelTextColor gets the color of the options panel text -func (gui *Gui) GetOptionsPanelTextColor() (gocui.Attribute, error) { - return GetGocuiStyle(gui.Config.UserConfig.Gui.Theme.OptionsTextColor), nil +func (gui *Gui) GetOptionsPanelTextColor() gocui.Attribute { + return GetGocuiStyle(gui.Config.UserConfig.Gui.Theme.OptionsTextColor) } // SetColorScheme sets the color scheme for the app based on the user config diff --git a/pkg/gui/view_helpers.go b/pkg/gui/view_helpers.go index a08b3a73..6e1a62cf 100644 --- a/pkg/gui/view_helpers.go +++ b/pkg/gui/view_helpers.go @@ -291,6 +291,10 @@ func (gui *Gui) trimmedContent(v *gocui.View) string { func (gui *Gui) currentViewName() string { currentView := gui.g.CurrentView() + // this can happen when the app is first starting up + if currentView == nil { + return gui.initiallyFocusedViewName() + } return currentView.Name() } @@ -383,3 +387,51 @@ func (gui *Gui) handleClick(v *gocui.View, itemCount int, selectedLine *int, han return handleSelect(gui.g, v) } + +func (gui *Gui) nextScreenMode() error { + if gui.currentViewName() == "main" { + gui.State.ScreenMode = prevIntInCycle([]WindowMaximisation{SCREEN_NORMAL, SCREEN_HALF, SCREEN_FULL}, gui.State.ScreenMode) + + return nil + } + + gui.State.ScreenMode = nextIntInCycle([]WindowMaximisation{SCREEN_NORMAL, SCREEN_HALF, SCREEN_FULL}, gui.State.ScreenMode) + + return nil +} + +func (gui *Gui) prevScreenMode() error { + if gui.currentViewName() == "main" { + gui.State.ScreenMode = nextIntInCycle([]WindowMaximisation{SCREEN_NORMAL, SCREEN_HALF, SCREEN_FULL}, gui.State.ScreenMode) + + return nil + } + + gui.State.ScreenMode = prevIntInCycle([]WindowMaximisation{SCREEN_NORMAL, SCREEN_HALF, SCREEN_FULL}, gui.State.ScreenMode) + + return nil +} + +func nextIntInCycle(sl []WindowMaximisation, current WindowMaximisation) WindowMaximisation { + for i, val := range sl { + if val == current { + if i == len(sl)-1 { + return sl[0] + } + return sl[i+1] + } + } + return sl[0] +} + +func prevIntInCycle(sl []WindowMaximisation, current WindowMaximisation) WindowMaximisation { + for i, val := range sl { + if val == current { + if i > 0 { + return sl[i-1] + } + return sl[len(sl)-1] + } + } + return sl[len(sl)-1] +} diff --git a/pkg/gui/views.go b/pkg/gui/views.go new file mode 100644 index 00000000..3f83dd29 --- /dev/null +++ b/pkg/gui/views.go @@ -0,0 +1,128 @@ +package gui + +import ( + "github.com/fatih/color" + "github.com/jesseduffield/gocui" + "github.com/samber/lo" +) + +type Views struct { + Project *gocui.View + Services *gocui.View + Containers *gocui.View + Images *gocui.View + Volumes *gocui.View + + Main *gocui.View + + Options *gocui.View + Confirmation *gocui.View + Menu *gocui.View + Information *gocui.View + AppStatus *gocui.View + Limit *gocui.View +} + +type viewNameMapping struct { + viewPtr **gocui.View + name string +} + +func (gui *Gui) orderedViews() []*gocui.View { + return lo.Map(gui.orderedViewNameMappings(), func(v viewNameMapping, _ int) *gocui.View { + return *v.viewPtr + }) +} + +func (gui *Gui) orderedViewNameMappings() []viewNameMapping { + return []viewNameMapping{ + // first layer. Ordering within this layer does not matter because there are + // no overlapping views + {viewPtr: &gui.Views.Project, name: "project"}, + {viewPtr: &gui.Views.Services, name: "services"}, + {viewPtr: &gui.Views.Containers, name: "containers"}, + {viewPtr: &gui.Views.Images, name: "images"}, + {viewPtr: &gui.Views.Volumes, name: "volumes"}, + + {viewPtr: &gui.Views.Main, name: "main"}, + + // bottom line + {viewPtr: &gui.Views.Options, name: "options"}, + {viewPtr: &gui.Views.AppStatus, name: "appStatus"}, + {viewPtr: &gui.Views.Information, name: "information"}, + + // popups. + {viewPtr: &gui.Views.Menu, name: "menu"}, + {viewPtr: &gui.Views.Confirmation, name: "confirmation"}, + + // this guy will cover everything else when it appears + {viewPtr: &gui.Views.Limit, name: "limit"}, + } +} + +func (gui *Gui) createAllViews() error { + var err error + for _, mapping := range gui.orderedViewNameMappings() { + *mapping.viewPtr, err = gui.prepareView(mapping.name) + if err != nil && err.Error() != UNKNOWN_VIEW_ERROR_MSG { + return err + } + (*mapping.viewPtr).FgColor = gocui.ColorDefault + } + + gui.Views.Main.Wrap = gui.Config.UserConfig.Gui.WrapMainPanel + // when you run a docker container with the -it flags (interactive mode) it adds carriage returns for some reason. This is not docker's fault, it's an os-level default. + gui.Views.Main.IgnoreCarriageReturns = true + + gui.Views.Project.Title = gui.Tr.ProjectTitle + + gui.Views.Services.Highlight = true + gui.Views.Services.Title = gui.Tr.ServicesTitle + + gui.Views.Containers.Highlight = true + if gui.Config.UserConfig.Gui.ShowAllContainers || !gui.DockerCommand.InDockerComposeProject { + gui.Views.Containers.Title = gui.Tr.ContainersTitle + } else { + gui.Views.Containers.Title = gui.Tr.StandaloneContainersTitle + } + + gui.Views.Images.Highlight = true + gui.Views.Images.Title = gui.Tr.ImagesTitle + + gui.Views.Volumes.Highlight = true + gui.Views.Volumes.Title = gui.Tr.VolumesTitle + + gui.Views.Options.Frame = false + gui.Views.Options.FgColor = gui.GetOptionsPanelTextColor() + + gui.Views.AppStatus.FgColor = gocui.ColorCyan + gui.Views.AppStatus.Frame = false + + gui.Views.Information.Frame = false + gui.Views.Information.FgColor = gocui.ColorGreen + + if err := gui.renderString(gui.g, "information", gui.getInformationContent()); err != nil { + return err + } + + gui.Views.Confirmation.Visible = false + gui.Views.Menu.Visible = false + + gui.Views.Limit.Visible = false + gui.Views.Limit.Title = gui.Tr.NotEnoughSpace + gui.Views.Limit.Wrap = true + + gui.waitForIntro.Done() + + return nil +} + +func (gui *Gui) getInformationContent() string { + informationStr := gui.Config.Version + if !gui.g.Mouse { + return informationStr + } + + donate := color.New(color.FgMagenta, color.Underline).Sprint(gui.Tr.Donate) + return donate + " " + informationStr +} diff --git a/pkg/gui/window.go b/pkg/gui/window.go new file mode 100644 index 00000000..42c12dd5 --- /dev/null +++ b/pkg/gui/window.go @@ -0,0 +1,7 @@ +package gui + +func (gui *Gui) currentWindow() string { + // at the moment, we only have one view per window in lazydocker, so we + // are using the view name as the window name + return gui.currentViewName() +} diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index cec206e0..62d9a1c4 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -105,6 +105,9 @@ type TranslationSet struct { No string Yes string + + LcNextScreenMode string + LcPrevScreenMode string } func englishSet() TranslationSet { @@ -217,5 +220,8 @@ func englishSet() TranslationSet { No: "no", Yes: "yes", + + LcNextScreenMode: "next screen mode (normal/half/fullscreen)", + LcPrevScreenMode: "prev screen mode", } } diff --git a/scripts/bump_gocui.sh b/scripts/bump_gocui.sh new file mode 100755 index 00000000..3e0adeba --- /dev/null +++ b/scripts/bump_gocui.sh @@ -0,0 +1,5 @@ +# Go's proxy servers are not very up-to-date so that's why we use `GOPROXY=direct` +# We specify the `awesome` branch to avoid the default behaviour of looking for a semver tag. +GOPROXY=direct go get -u github.com/jesseduffield/gocui@awesome && go mod vendor && go mod tidy + +# Note to self if you ever want to fork a repo be sure to use this same approach: it's important to use the branch name (e.g. master) diff --git a/scripts/bump_lazycore.sh b/scripts/bump_lazycore.sh new file mode 100755 index 00000000..810e5f08 --- /dev/null +++ b/scripts/bump_lazycore.sh @@ -0,0 +1,5 @@ +# Go's proxy servers are not very up-to-date so that's why we use `GOPROXY=direct` +# We specify the `awesome` branch to avoid the default behaviour of looking for a semver tag. +GOPROXY=direct go get -u github.com/jesseduffield/lazycore@master && go mod vendor && go mod tidy + +# Note to self if you ever want to fork a repo be sure to use this same approach: it's important to use the branch name (e.g. master) diff --git a/vendor/github.com/jesseduffield/lazycore/LICENSE b/vendor/github.com/jesseduffield/lazycore/LICENSE new file mode 100644 index 00000000..2a7175dc --- /dev/null +++ b/vendor/github.com/jesseduffield/lazycore/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Jesse Duffield + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/jesseduffield/lazycore/pkg/boxlayout/boxlayout.go b/vendor/github.com/jesseduffield/lazycore/pkg/boxlayout/boxlayout.go new file mode 100644 index 00000000..c516cbba --- /dev/null +++ b/vendor/github.com/jesseduffield/lazycore/pkg/boxlayout/boxlayout.go @@ -0,0 +1,211 @@ +package boxlayout + +import ( + "github.com/jesseduffield/lazycore/pkg/utils" + "github.com/samber/lo" +) + +type Dimensions struct { + X0 int + X1 int + Y0 int + Y1 int +} + +type Direction int + +const ( + ROW Direction = iota + COLUMN +) + +// to give a high-level explanation of what's going on here. We layout our windows by arranging a bunch of boxes in the available space. +// If a box has children, it needs to specify how it wants to arrange those children: ROW or COLUMN. +// If a box represents a window, you can put the window name in the Window field. +// When determining how to divvy-up the available height (for row children) or width (for column children), we first +// give the boxes with a static `size` the space that they want. Then we apportion +// the remaining space based on the weights of the dynamic boxes (you can't define +// both size and weight at the same time: you gotta pick one). If there are two +// boxes, one with weight 1 and the other with weight 2, the first one gets 33% +// of the available space and the second one gets the remaining 66% + +type Box struct { + // Direction decides how the children boxes are laid out. ROW means the children will each form a row i.e. that they will be stacked on top of eachother. + Direction Direction + + // function which takes the width and height assigned to the box and decides which orientation it will have + ConditionalDirection func(width int, height int) Direction + + Children []*Box + + // function which takes the width and height assigned to the box and decides the layout of the children. + ConditionalChildren func(width int, height int) []*Box + + // Window refers to the name of the window this box represents, if there is one + Window string + + // static Size. If parent box's direction is ROW this refers to height, otherwise width + Size int + + // dynamic size. Once all statically sized children have been considered, Weight decides how much of the remaining space will be taken up by the box + // TODO: consider making there be one int and a type enum so we can't have size and Weight simultaneously defined + Weight int +} + +func ArrangeWindows(root *Box, x0, y0, width, height int) map[string]Dimensions { + children := root.getChildren(width, height) + if len(children) == 0 { + // leaf node + if root.Window != "" { + dimensionsForWindow := Dimensions{X0: x0, Y0: y0, X1: x0 + width - 1, Y1: y0 + height - 1} + return map[string]Dimensions{root.Window: dimensionsForWindow} + } + return map[string]Dimensions{} + } + + direction := root.getDirection(width, height) + + var availableSize int + if direction == COLUMN { + availableSize = width + } else { + availableSize = height + } + + sizes := calcSizes(children, availableSize) + + result := map[string]Dimensions{} + offset := 0 + for i, child := range children { + boxSize := sizes[i] + + var resultForChild map[string]Dimensions + if direction == COLUMN { + resultForChild = ArrangeWindows(child, x0+offset, y0, boxSize, height) + } else { + resultForChild = ArrangeWindows(child, x0, y0+offset, width, boxSize) + } + + result = mergeDimensionMaps(result, resultForChild) + offset += boxSize + } + + return result +} + +func calcSizes(boxes []*Box, availableSpace int) []int { + normalizedWeights := normalizeWeights(lo.Map(boxes, func(box *Box, _ int) int { return box.Weight })) + + totalWeight := 0 + reservedSpace := 0 + for i, box := range boxes { + if box.isStatic() { + reservedSpace += box.Size + } else { + totalWeight += normalizedWeights[i] + } + } + + dynamicSpace := utils.Max(0, availableSpace-reservedSpace) + + unitSize := 0 + extraSpace := 0 + if totalWeight > 0 { + unitSize = dynamicSpace / totalWeight + extraSpace = dynamicSpace % totalWeight + } + + result := make([]int, len(boxes)) + for i, box := range boxes { + if box.isStatic() { + // assuming that only one static child can have a size greater than the + // available space. In that case we just crop the size to what's available + result[i] = utils.Min(availableSpace, box.Size) + } else { + result[i] = unitSize * normalizedWeights[i] + } + } + + // distribute the remainder across dynamic boxes. + for extraSpace > 0 { + for i, weight := range normalizedWeights { + if weight > 0 { + result[i]++ + extraSpace-- + normalizedWeights[i]-- + + if extraSpace == 0 { + break + } + } + } + } + + return result +} + +// removes common multiple from weights e.g. if we get 2, 4, 4 we return 1, 2, 2. +func normalizeWeights(weights []int) []int { + if len(weights) == 0 { + return []int{} + } + + // to spare us some computation we'll exit early if any of our weights is 1 + if lo.SomeBy(weights, func(weight int) bool { return weight == 1 }) { + return weights + } + + // map weights to factorSlices and find the lowest common factor + positiveWeights := lo.Filter(weights, func(weight int, _ int) bool { return weight > 0 }) + factorSlices := lo.Map(positiveWeights, func(weight int, _ int) []int { return calcFactors(weight) }) + commonFactors := factorSlices[0] + for _, factors := range factorSlices { + commonFactors = lo.Intersect(commonFactors, factors) + } + + if len(commonFactors) == 0 { + return weights + } + + newWeights := lo.Map(weights, func(weight int, _ int) int { return weight / commonFactors[0] }) + + return normalizeWeights(newWeights) +} + +func calcFactors(n int) []int { + factors := []int{} + for i := 2; i <= n; i++ { + if n%i == 0 { + factors = append(factors, i) + } + } + return factors +} + +func (b *Box) isStatic() bool { + return b.Size > 0 +} + +func (b *Box) getDirection(width int, height int) Direction { + if b.ConditionalDirection != nil { + return b.ConditionalDirection(width, height) + } + return b.Direction +} + +func (b *Box) getChildren(width int, height int) []*Box { + if b.ConditionalChildren != nil { + return b.ConditionalChildren(width, height) + } + return b.Children +} + +func mergeDimensionMaps(a map[string]Dimensions, b map[string]Dimensions) map[string]Dimensions { + result := map[string]Dimensions{} + for _, dimensionMap := range []map[string]Dimensions{a, b} { + for k, v := range dimensionMap { + result[k] = v + } + } + return result +} diff --git a/vendor/github.com/jesseduffield/lazycore/pkg/utils/utils.go b/vendor/github.com/jesseduffield/lazycore/pkg/utils/utils.go new file mode 100644 index 00000000..9fa93376 --- /dev/null +++ b/vendor/github.com/jesseduffield/lazycore/pkg/utils/utils.go @@ -0,0 +1,16 @@ +package utils + +// Min returns the minimum of two integers +func Min(x, y int) int { + if x < y { + return x + } + return y +} + +func Max(x, y int) int { + if x > y { + return x + } + return y +} diff --git a/vendor/github.com/samber/lo/CHANGELOG.md b/vendor/github.com/samber/lo/CHANGELOG.md index 2eaa45c9..9ba32f1b 100644 --- a/vendor/github.com/samber/lo/CHANGELOG.md +++ b/vendor/github.com/samber/lo/CHANGELOG.md @@ -2,6 +2,128 @@ @samber: I sometimes forget to update this file. Ping me on [Twitter](https://twitter.com/samuelberthe) or open an issue in case of error. We need to keep a clear changelog for easier lib upgrade. +## 1.31.0 (2022-10-06) + +Adding: + +- lo.SliceToChannel +- lo.Generator +- lo.Batch +- lo.BatchWithTimeout + +## 1.30.1 (2022-10-06) + +Fix: + +- lo.Try1: remove generic type +- lo.Validate: format error properly + +## 1.30.0 (2022-10-04) + +Adding: + +- lo.TernaryF +- lo.Validate + +## 1.29.0 (2022-10-02) + +Adding: + +- lo.ErrorAs +- lo.TryOr +- lo.TryOrX + +## 1.28.0 (2022-09-05) + +Adding: + +- lo.ChannelDispatcher with 6 dispatching strategies: + - lo.DispatchingStrategyRoundRobin + - lo.DispatchingStrategyRandom + - lo.DispatchingStrategyWeightedRandom + - lo.DispatchingStrategyFirst + - lo.DispatchingStrategyLeast + - lo.DispatchingStrategyMost + +## 1.27.1 (2022-08-15) + +Bugfix: + +- Removed comparable constraint for lo.FindKeyBy + +## 1.27.0 (2022-07-29) + +Breaking: + +- Change of MapToSlice prototype: `MapToSlice[K comparable, V any, R any](in map[K]V, iteratee func(V, K) R) []R` -> `MapToSlice[K comparable, V any, R any](in map[K]V, iteratee func(K, V) R) []R` + +Added: + +- lo.ChunkString +- lo.SliceToMap (alias to lo.Associate) + +## 1.26.0 (2022-07-24) + +Adding: + +- lo.Associate +- lo.ReduceRight +- lo.FromPtrOr +- lo.MapToSlice +- lo.IsSorted +- lo.IsSortedByKey + +## 1.25.0 (2022-07-04) + +Adding: + +- lo.FindUniques +- lo.FindUniquesBy +- lo.FindDuplicates +- lo.FindDuplicatesBy +- lo.IsNotEmpty + +## 1.24.0 (2022-07-04) + +Adding: + +- lo.Without +- lo.WithoutEmpty + +## 1.23.0 (2022-07-04) + +Adding: + +- lo.FindKey +- lo.FindKeyBy + +## 1.22.0 (2022-07-04) + +Adding: + +- lo.Slice +- lo.FromPtr +- lo.IsEmpty +- lo.Compact +- lo.ToPairs: alias to lo.Entries +- lo.FromPairs: alias to lo.FromEntries +- lo.Partial + +Change: + +- lo.Must + lo.MustX: add context to panic message + +Fix: + +- lo.Nth: out of bound exception (#137) + +## 1.21.0 (2022-05-10) + +Adding: + +- lo.ToAnySlice +- lo.FromAnySlice + ## 1.20.0 (2022-05-02) Adding: @@ -55,7 +177,7 @@ Improvement: Adding: -- lo.Colaesce +- lo.Coalesce ## 1.13.0 (2022-04-14) diff --git a/vendor/github.com/samber/lo/Makefile b/vendor/github.com/samber/lo/Makefile index 91416187..57bb4915 100644 --- a/vendor/github.com/samber/lo/Makefile +++ b/vendor/github.com/samber/lo/Makefile @@ -1,10 +1,5 @@ BIN=go -# BIN=go1.18beta1 - -go1.18beta1: - go install golang.org/dl/go1.18beta1@latest - go1.18beta1 download build: ${BIN} build -v ./... @@ -20,7 +15,7 @@ watch-bench: reflex -t 50ms -s -- sh -c 'go test -benchmem -count 3 -bench ./...' coverage: - ${BIN} test -v -coverprofile cover.out . + ${BIN} test -v -coverprofile=cover.out -covermode=atomic . ${BIN} tool cover -html=cover.out -o cover.html # tools @@ -31,7 +26,7 @@ tools: ${BIN} install github.com/jondot/goweight@latest ${BIN} install github.com/golangci/golangci-lint/cmd/golangci-lint@latest ${BIN} get -t -u golang.org/x/tools/cmd/cover - ${BIN} get -t -u github.com/sonatype-nexus-community/nancy@latest + ${BIN} install github.com/sonatype-nexus-community/nancy@latest go mod tidy lint: @@ -40,11 +35,9 @@ lint-fix: golangci-lint run --timeout 60s --max-same-issues 50 --fix ./... audit: tools - ${BIN} mod tidy ${BIN} list -json -m all | nancy sleuth outdated: tools - ${BIN} mod tidy ${BIN} list -u -m -json all | go-mod-outdated -update -direct weight: tools diff --git a/vendor/github.com/samber/lo/README.md b/vendor/github.com/samber/lo/README.md index 10107ce0..49cbe49d 100644 --- a/vendor/github.com/samber/lo/README.md +++ b/vendor/github.com/samber/lo/README.md @@ -1,10 +1,12 @@ # lo -![Build Status](https://github.com/samber/lo/actions/workflows/go.yml/badge.svg) +[![tag](https://img.shields.io/github/tag/samber/lo.svg)](https://github.com/samber/lo/releases) [![GoDoc](https://godoc.org/github.com/samber/lo?status.svg)](https://pkg.go.dev/github.com/samber/lo) +![Build Status](https://github.com/samber/lo/actions/workflows/go.yml/badge.svg) [![Go report](https://goreportcard.com/badge/github.com/samber/lo)](https://goreportcard.com/report/github.com/samber/lo) +[![codecov](https://codecov.io/gh/samber/lo/branch/master/graph/badge.svg)](https://codecov.io/gh/samber/lo) -✨ **`lo` is a Lodash-style Go library based on Go 1.18+ Generics.** +✨ **`samber/lo` is a Lodash-style Go library based on Go 1.18+ Generics.** This project started as an experiment with the new generics implementation. It may look like [Lodash](https://github.com/lodash/lodash) in some aspects. I used to code with the fantastic ["go-funk"](https://github.com/thoas/go-funk) package, but "go-funk" uses reflection and therefore is not typesafe. @@ -12,7 +14,12 @@ As expected, benchmarks demonstrate that generics will be much faster than imple In the future, 5 to 10 helpers will overlap with those coming into the Go standard library (under package names `slices` and `maps`). I feel this library is legitimate and offers many more valuable abstractions. -### Why this name? +**See also:** + +- [samber/do](https://github.com/samber/do): A dependency injection toolkit based on Go 1.18+ Generics +- [samber/mo](https://github.com/samber/mo): Monads based on Go 1.18+ Generics (Option, Result, Either...) + +**Why this name?** I wanted a **short name**, similar to "Lodash" and no Go package currently uses this name. @@ -52,126 +59,187 @@ GoDoc: [https://godoc.org/github.com/samber/lo](https://godoc.org/github.com/sam Supported helpers for slices: -- Filter -- Map -- FilterMap -- FlatMap -- Reduce -- ForEach -- Times -- Uniq -- UniqBy -- GroupBy -- Chunk -- PartitionBy -- Flatten -- Shuffle -- Reverse -- Fill -- Repeat -- KeyBy -- Drop -- DropRight -- DropWhile -- DropRightWhile -- Reject -- Count -- CountBy +- [Filter](#filter) +- [Map](#map) +- [FilterMap](#filtermap) +- [FlatMap](#flatmap) +- [Reduce](#reduce) +- [ReduceRight](#reduceright) +- [ForEach](#foreach) +- [Times](#times) +- [Uniq](#uniq) +- [UniqBy](#uniqby) +- [GroupBy](#groupby) +- [Chunk](#chunk) +- [PartitionBy](#partitionby) +- [Flatten](#flatten) +- [Shuffle](#shuffle) +- [Reverse](#reverse) +- [Fill](#fill) +- [Repeat](#repeat) +- [RepeatBy](#repeatby) +- [KeyBy](#keyby) +- [Associate / SliceToMap](#associate-alias-slicetomap) +- [Drop](#drop) +- [DropRight](#dropright) +- [DropWhile](#dropwhile) +- [DropRightWhile](#droprightwhile) +- [Reject](#reject) +- [Count](#count) +- [CountBy](#countby) +- [Subset](#subset) +- [Slice](#slice) +- [Replace](#replace) +- [ReplaceAll](#replaceall) +- [Compact](#compact) +- [IsSorted](#issorted) +- [IsSortedByKey](#issortedbykey) Supported helpers for maps: -- Keys -- Values -- PickBy -- PickByKeys -- PickByValues -- OmitBy -- OmitByKeys -- OmitByValues -- Entries -- FromEntries -- Invert -- Assign (merge of maps) -- MapKeys -- MapValues +- [Keys](#keys) +- [Values](#values) +- [PickBy](#pickby) +- [PickByKeys](#pickbykeys) +- [PickByValues](#pickbyvalues) +- [OmitBy](#omitby) +- [OmitByKeys](#omitbykeys) +- [OmitByValues](#omitbyvalues) +- [Entries / ToPairs](#entries-alias-topairs) +- [FromEntries / FromPairs](#fromentries-alias-frompairs) +- [Invert](#invert) +- [Assign (merge of maps)](#assign) +- [MapKeys](#mapkeys) +- [MapValues](#mapvalues) +- [MapToSlice](#maptoslice) Supported math helpers: -- Range / RangeFrom / RangeWithSteps -- Clamp -- SumBy +- [Range / RangeFrom / RangeWithSteps](#range--rangefrom--rangewithsteps) +- [Clamp](#clamp) +- [SumBy](#sumby) Supported helpers for strings: -- Substring -- RuneLength +- [Substring](#substring) +- [ChunkString](#chunkstring) +- [RuneLength](#runelength) Supported helpers for tuples: -- T2 -> T9 -- Unpack2 -> Unpack9 -- Zip2 -> Zip9 -- Unzip2 -> Unzip9 +- [T2 -> T9](#t2---t9) +- [Unpack2 -> Unpack9](#unpack2---unpack9) +- [Zip2 -> Zip9](#zip2---zip9) +- [Unzip2 -> Unzip9](#unzip2---unzip9) + +Supported helpers for channels: + +- [ChannelDispatcher](#channeldispatcher) +- [SliceToChannel](#slicetochannel) +- [Generator](#generator) +- [Batch](#batch) +- [BatchWithTimeout](#batchwithtimeout) Supported intersection helpers: -- Contains -- ContainsBy -- Every -- EveryBy -- Some -- SomeBy -- None -- NoneBy -- Intersect -- Difference -- Union +- [Contains](#contains) +- [ContainsBy](#containsby) +- [Every](#every) +- [EveryBy](#everyby) +- [Some](#some) +- [SomeBy](#someby) +- [None](#none) +- [NoneBy](#noneby) +- [Intersect](#intersect) +- [Difference](#difference) +- [Union](#union) +- [Without](#without) +- [WithoutEmpty](#withoutempty) Supported search helpers: -- IndexOf -- LastIndexOf -- Find -- FindIndexOf -- FindLastIndexOf -- Min -- MinBy -- Max -- MaxBy -- Last -- Nth -- Sample -- Samples +- [IndexOf](#indexof) +- [LastIndexOf](#lastindexof) +- [Find](#find) +- [FindIndexOf](#findindexof) +- [FindLastIndexOf](#findlastindexof) +- [FindKey](#findkey) +- [FindKeyBy](#findkeyby) +- [FindUniques](#finduniques) +- [FindUniquesBy](#finduniquesby) +- [FindDuplicates](#findduplicates) +- [FindDuplicatesBy](#findduplicatesby) +- [Min](#min) +- [MinBy](#minby) +- [Max](#max) +- [MaxBy](#maxby) +- [Last](#last) +- [Nth](#nth) +- [Sample](#sample) +- [Samples](#samples) -Other functional programming helpers: +Conditional helpers: -- Ternary (1 line if/else statement) -- If / ElseIf / Else -- Switch / Case / Default -- ToPtr -- ToSlicePtr -- Empty -- Coalesce +- [Ternary](#ternary) +- [TernaryF](#ternaryf) +- [If / ElseIf / Else](#if--elseif--else) +- [Switch / Case / Default](#switch--case--default) + +Type manipulation helpers: + +- [ToPtr](#toptr) +- [FromPtr](#fromptr) +- [FromPtrOr](#fromptror) +- [ToSlicePtr](#tosliceptr) +- [ToAnySlice](#toanyslice) +- [FromAnySlice](#fromanyslice) +- [Empty](#empty) +- [IsEmpty](#isempty) +- [IsNotEmpty](#isnotempty) +- [Coalesce](#coalesce) + +Function helpers: + +- [Partial](#partial) Concurrency helpers: -- Attempt -- AttemptWithDelay -- Debounce -- Async +- [Attempt](#attempt) +- [AttemptWithDelay](#attemptwithdelay) +- [Debounce](#debounce) +- [Synchronize](#synchronize) +- [Async](#async) Error handling: -- Must -- Try -- TryCatch -- TryWithErrorValue -- TryCatchWithErrorValue +- [Validate](#validate) +- [Must](#must) +- [Try](#try) +- [Try1 -> Try6](#try0-6) +- [TryOr](#tryor) +- [TryOr1 -> TryOr6](#tryor0-6) +- [TryCatch](#trycatch) +- [TryWithErrorValue](#trywitherrorvalue) +- [TryCatchWithErrorValue](#trycatchwitherrorvalue) +- [ErrorsAs](#errorsas) Constraints: - Clonable +### Filter + +Iterates over a collection and returns an array of all the elements the predicate function returns `true` for. + +```go +even := lo.Filter[int]([]int{1, 2, 3, 4}, func(x int, index int) bool { + return x%2 == 0 +}) +// []int{2, 4} +``` + +[[play](https://go.dev/play/p/Apjg3WeSi7K)] + ### Map Manipulates a slice of one type and transforms it into a slice of another type: @@ -179,12 +247,14 @@ Manipulates a slice of one type and transforms it into a slice of another type: ```go import "github.com/samber/lo" -lo.Map[int64, string]([]int64{1, 2, 3, 4}, func(x int64, _ int) string { +lo.Map[int64, string]([]int64{1, 2, 3, 4}, func(x int64, index int) string { return strconv.FormatInt(x, 10) }) // []string{"1", "2", "3", "4"} ``` +[[play](https://go.dev/play/p/OkPcYAhBo0D)] + Parallel processing: like `lo.Map()`, but the mapper function is called in a goroutine. Results are returned in the same order. ```go @@ -196,20 +266,6 @@ lop.Map[int64, string]([]int64{1, 2, 3, 4}, func(x int64, _ int) string { // []string{"1", "2", "3", "4"} ``` -### FlatMap - -Manipulates a slice and transforms and flattens it to a slice of another type. - -```go -lo.FlatMap[int, string]([]int{0, 1, 2}, func(x int, _ int) []string { - return []string{ - strconv.FormatInt(x, 10), - strconv.FormatInt(x, 10), - } -}) -// []string{"0", "0", "1", "1", "2", "2"} -``` - ### FilterMap Returns a slice which obtained after both filtering and mapping using the given callback function. @@ -226,36 +282,23 @@ matching := lo.FilterMap[string, string]([]string{"cpu", "gpu", "mouse", "keyboa // []string{"xpu", "xpu"} ``` -### Filter +[[play](https://go.dev/play/p/-AuYXfy7opz)] -Iterates over a collection and returns an array of all the elements the predicate function returns `true` for. +### FlatMap + +Manipulates a slice and transforms and flattens it to a slice of another type. ```go -even := lo.Filter[int]([]int{1, 2, 3, 4}, func(x int, _ int) bool { - return x%2 == 0 +lo.FlatMap[int, string]([]int{0, 1, 2}, func(x int, _ int) []string { + return []string{ + strconv.FormatInt(x, 10), + strconv.FormatInt(x, 10), + } }) -// []int{2, 4} +// []string{"0", "0", "1", "1", "2", "2"} ``` -### Contains - -Returns true if an element is present in a collection. - -```go -present := lo.Contains[int]([]int{0, 1, 2, 3, 4, 5}, 5) -// true -``` - -### ContainsBy - -Returns true if the predicate function returns `true`. - -```go -present := lo.ContainsBy[int]([]int{0, 1, 2, 3, 4, 5}, func(x int) bool { - return x == 3 -}) -// true -``` +[[play](https://go.dev/play/p/YSoYmQTA8-U)] ### Reduce @@ -268,6 +311,21 @@ sum := lo.Reduce[int, int]([]int{1, 2, 3, 4}, func(agg int, item int, _ int) int // 10 ``` +[[play](https://go.dev/play/p/R4UHXZNaaUG)] + +### ReduceRight + +Like `lo.Reduce` except that it iterates over elements of collection from right to left. + +```go +result := lo.ReduceRight[[]int, []int]([][]int{{0, 1}, {2, 3}, {4, 5}}, func(agg []int, item []int, _ int) []int { + return append(agg, item...) +}, []int{}) +// []int{4, 5, 2, 3, 0, 1} +``` + +[[play](https://go.dev/play/p/Fq3W70l7wXF)] + ### ForEach Iterates over elements of a collection and invokes the function over each element. @@ -281,6 +339,8 @@ lo.ForEach[string]([]string{"hello", "world"}, func(x string, _ int) { // prints "hello\nworld\n" ``` +[[play](https://go.dev/play/p/oofyiUPRf8t)] + Parallel processing: like `lo.ForEach()`, but the callback is called as a goroutine. ```go @@ -305,6 +365,8 @@ lo.Times[string](3, func(i int) string { // []string{"0", "1", "2"} ``` +[[play](https://go.dev/play/p/vgQj3Glr6lT)] + Parallel processing: like `lo.Times()`, but callback is called in goroutine. ```go @@ -325,6 +387,8 @@ uniqValues := lo.Uniq[int]([]int{1, 2, 2, 1}) // []int{1, 2} ``` +[[play](https://go.dev/play/p/DTzbeXZ6iEN)] + ### UniqBy Returns a duplicate-free version of an array, in which only the first occurrence of each element is kept. The order of result values is determined by the order they occur in the array. It accepts `iteratee` which is invoked for each element in array to generate the criterion by which uniqueness is computed. @@ -336,6 +400,8 @@ uniqValues := lo.UniqBy[int, int]([]int{0, 1, 2, 3, 4, 5}, func(i int) int { // []int{0, 1, 2} ``` +[[play](https://go.dev/play/p/g42Z3QSb53u)] + ### GroupBy Returns an object composed of keys generated from the results of running each element of collection through iteratee. @@ -349,6 +415,8 @@ groups := lo.GroupBy[int, int]([]int{0, 1, 2, 3, 4, 5}, func(i int) int { // map[int][]int{0: []int{0, 3}, 1: []int{1, 4}, 2: []int{2, 5}} ``` +[[play](https://go.dev/play/p/XnQBd_v6brd)] + Parallel processing: like `lo.GroupBy()`, but callback is called in goroutine. ```go @@ -378,6 +446,8 @@ lo.Chunk[int]([]int{0}, 2) // [][]int{{0}} ``` +[[play](https://go.dev/play/p/EeKl0AuTehH)] + ### PartitionBy Returns an array of elements split into groups. The order of grouped values is determined by the order they occur in collection. The grouping is generated from the results of running each element of collection through iteratee. @@ -396,12 +466,14 @@ partitions := lo.PartitionBy[int, string]([]int{-2, -1, 0, 1, 2, 3, 4, 5}, func( // [][]int{{-2, -1}, {0, 2, 4}, {1, 3, 5}} ``` +[[play](https://go.dev/play/p/NfQ_nGjkgXW)] + Parallel processing: like `lo.PartitionBy()`, but callback is called in goroutine. Results are returned in the same order. ```go import lop "github.com/samber/lo/parallel" -partitions := lo.PartitionBy[int, string]([]int{-2, -1, 0, 1, 2, 3, 4, 5}, func(x int) string { +partitions := lop.PartitionBy[int, string]([]int{-2, -1, 0, 1, 2, 3, 4, 5}, func(x int) string { if x < 0 { return "negative" } else if x%2 == 0 { @@ -421,24 +493,30 @@ flat := lo.Flatten[int]([][]int{{0, 1}, {2, 3, 4, 5}}) // []int{0, 1, 2, 3, 4, 5} ``` +[[play](https://go.dev/play/p/rbp9ORaMpjw)] + ### Shuffle Returns an array of shuffled values. Uses the Fisher-Yates shuffle algorithm. ```go randomOrder := lo.Shuffle[int]([]int{0, 1, 2, 3, 4, 5}) -// []int{0, 1, 2, 3, 4, 5} +// []int{1, 4, 0, 3, 5, 2} ``` +[[play](https://go.dev/play/p/Qp73bnTDnc7)] + ### Reverse Reverses array so that the first element becomes the last, the second element becomes the second to last, and so on. ```go -reverseOder := lo.Reverse[int]([]int{0, 1, 2, 3, 4, 5}) +reverseOrder := lo.Reverse[int]([]int{0, 1, 2, 3, 4, 5}) // []int{5, 4, 3, 2, 1, 0} ``` +[[play](https://go.dev/play/p/fhUMLvZ7vS6)] + ### Fill Fills elements of array with `initial` value. @@ -456,6 +534,8 @@ initializedSlice := lo.Fill[foo]([]foo{foo{"a"}, foo{"a"}}, foo{"b"}) // []foo{foo{"b"}, foo{"b"}} ``` +[[play](https://go.dev/play/p/VwR34GzqEub)] + ### Repeat Builds a slice with N copies of initial value. @@ -473,22 +553,26 @@ slice := lo.Repeat[foo](2, foo{"a"}) // []foo{foo{"a"}, foo{"a"}} ``` +[[play](https://go.dev/play/p/g3uHXbmc3b6)] + ### RepeatBy Builds a slice with values returned by N calls of callback. ```go -slice := lo.RepeatBy[int](0, func (i int) int { - return math.Pow(i, 2) +slice := lo.RepeatBy[string](0, func (i int) string { + return strconv.FormatInt(int64(math.Pow(float64(i), 2)), 10) }) // []int{} -slice := lo.RepeatBy[int](5, func (i int) int { - return math.Pow(i, 2) +slice := lo.RepeatBy[string](5, func(i int) string { + return strconv.FormatInt(int64(math.Pow(float64(i), 2)), 10) }) // []int{0, 1, 4, 9, 16} ``` +[[play](https://go.dev/play/p/ozZLCtX_hNU)] + ### KeyBy Transforms a slice or an array of structs to a map based on a pivot callback. @@ -513,6 +597,26 @@ result := lo.KeyBy[string, Character](characters, func(char Character) string { //map[a:{dir:left code:97} d:{dir:right code:100}] ``` +[[play](https://go.dev/play/p/mdaClUAT-zZ)] + +### Associate (alias: SliceToMap) + +Returns a map containing key-value pairs provided by transform function applied to elements of the given slice. +If any of two pairs would have the same key the last one gets added to the map. + +The order of keys in returned map is not specified and is not guaranteed to be the same from the original array. + +```go +in := []*foo{{baz: "apple", bar: 1}, {baz: "banana", bar: 2}} + +aMap := lo.Associate[*foo, string, int](in, func (f *foo) (string, int) { + return f.baz, f.bar +}) +// map[string][int]{ "apple":1, "banana":2 } +``` + +[[play](https://go.dev/play/p/WHa2CfMO3Lr)] + ### Drop Drops n elements from the beginning of a slice or array. @@ -522,6 +626,8 @@ l := lo.Drop[int]([]int{0, 1, 2, 3, 4, 5}, 2) // []int{2, 3, 4, 5} ``` +[[play](https://go.dev/play/p/JswS7vXRJP2)] + ### DropRight Drops n elements from the end of a slice or array. @@ -531,6 +637,8 @@ l := lo.DropRight[int]([]int{0, 1, 2, 3, 4, 5}, 2) // []int{0, 1, 2, 3} ``` +[[play](https://go.dev/play/p/GG0nXkSJJa3)] + ### DropWhile Drop elements from the beginning of a slice or array while the predicate returns true. @@ -542,6 +650,8 @@ l := lo.DropWhile[string]([]string{"a", "aa", "aaa", "aa", "aa"}, func(val strin // []string{"aaa", "aa", "aa"} ``` +[[play](https://go.dev/play/p/7gBPYw2IK16)] + ### DropRightWhile Drop elements from the end of a slice or array while the predicate returns true. @@ -553,6 +663,8 @@ l := lo.DropRightWhile[string]([]string{"a", "aa", "aaa", "aa", "aa"}, func(val // []string{"a", "aa", "aaa"} ``` +[[play](https://go.dev/play/p/3-n71oEC0Hz)] + ### Reject The opposite of Filter, this method returns the elements of collection that predicate does not return truthy for. @@ -564,6 +676,8 @@ odd := lo.Reject[int]([]int{1, 2, 3, 4}, func(x int, _ int) bool { // []int{1, 3} ``` +[[play](https://go.dev/play/p/YkLMODy1WEL)] + ### Count Counts the number of elements in the collection that compare equal to value. @@ -573,6 +687,8 @@ count := lo.Count[int]([]int{1, 5, 1}, 1) // 2 ``` +[[play](https://go.dev/play/p/Y3FlK54yveC)] + ### CountBy Counts the number of elements in the collection for which predicate is true. @@ -584,9 +700,11 @@ count := lo.CountBy[int]([]int{1, 5, 1}, func(i int) bool { // 2 ``` +[[play](https://go.dev/play/p/ByQbNYQQi4X)] + ### Subset -Return part of a slice. +Returns a copy of a slice from `offset` up to `length` elements. Like `slice[start:start+length]`, but does not panic on overflow. ```go in := []int{0, 1, 2, 3, 4} @@ -601,6 +719,30 @@ sub := lo.Subset(in, -2, math.MaxUint) // []int{3, 4} ``` +[[play](https://go.dev/play/p/tOQu1GhFcog)] + +### Slice + +Returns a copy of a slice from `start` up to, but not including `end`. Like `slice[start:end]`, but does not panic on overflow. + +```go +in := []int{0, 1, 2, 3, 4} + +slice := lo.Slice(in, 0, 5) +// []int{0, 1, 2, 3, 4} + +slice := lo.Slice(in, 2, 3) +// []int{2} + +slice := lo.Slice(in, 2, 6) +// []int{2, 3, 4} + +slice := lo.Slice(in, 4, 3) +// []int{} +``` + +[[play](https://go.dev/play/p/8XWYhfMMA1h)] + ### Replace Returns a copy of the slice with the first n non-overlapping instances of old replaced by new. @@ -621,6 +763,8 @@ slice := lo.Replace(in, 0, 42, -1) // []int{42, 1, 42, 1, 2, 3, 42} ``` +[[play](https://go.dev/play/p/XfPzmf9gql6)] + ### ReplaceAll Returns a copy of the slice with all non-overlapping instances of old replaced by new. @@ -635,15 +779,56 @@ slice := lo.ReplaceAll(in, -1, 42) // []int{0, 1, 0, 1, 2, 3, 0} ``` +[[play](https://go.dev/play/p/a9xZFUHfYcV)] + +### Compact + +Returns a slice of all non-zero elements. + +```go +in := []string{"", "foo", "", "bar", ""} + +slice := lo.Compact[string](in) +// []string{"foo", "bar"} +``` + +[[play](https://go.dev/play/p/tXiy-iK6PAc)] + +### IsSorted + +Checks if a slice is sorted. + +```go +slice := lo.IsSorted([]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}) +// true +``` + +[[play](https://go.dev/play/p/mc3qR-t4mcx)] + +### IsSortedByKey + +Checks if a slice is sorted by iteratee. + +```go +slice := lo.IsSortedByKey([]string{"a", "bb", "ccc"}, func(s string) int { + return len(s) +}) +// true +``` + +[[play](https://go.dev/play/p/wiG6XyBBu49)] + ### Keys Creates an array of the map keys. ```go keys := lo.Keys[string, int](map[string]int{"foo": 1, "bar": 2}) -// []string{"bar", "foo"} +// []string{"foo", "bar"} ``` +[[play](https://go.dev/play/p/Uu11fHASqrU)] + ### Values Creates an array of the map values. @@ -653,6 +838,8 @@ values := lo.Values[string, int](map[string]int{"foo": 1, "bar": 2}) // []int{1, 2} ``` +[[play](https://go.dev/play/p/nnRTQkzQfF6)] + ### PickBy Returns same map type filtered by given predicate. @@ -664,6 +851,8 @@ m := lo.PickBy[string, int](map[string]int{"foo": 1, "bar": 2, "baz": 3}, func(k // map[string]int{"foo": 1, "baz": 3} ``` +[[play](https://go.dev/play/p/kdg8GR_QMmf)] + ### PickByKeys Returns same map type filtered by given keys. @@ -673,6 +862,8 @@ m := lo.PickByKeys[string, int](map[string]int{"foo": 1, "bar": 2, "baz": 3}, [] // map[string]int{"foo": 1, "baz": 3} ``` +[[play](https://go.dev/play/p/R1imbuci9qU)] + ### PickByValues Returns same map type filtered by given values. @@ -682,6 +873,8 @@ m := lo.PickByValues[string, int](map[string]int{"foo": 1, "bar": 2, "baz": 3}, // map[string]int{"foo": 1, "baz": 3} ``` +[[play](https://go.dev/play/p/1zdzSvbfsJc)] + ### OmitBy Returns same map type filtered by given predicate. @@ -693,6 +886,8 @@ m := lo.OmitBy[string, int](map[string]int{"foo": 1, "bar": 2, "baz": 3}, func(k // map[string]int{"bar": 2} ``` +[[play](https://go.dev/play/p/EtBsR43bdsd)] + ### OmitByKeys Returns same map type filtered by given keys. @@ -702,6 +897,8 @@ m := lo.OmitByKeys[string, int](map[string]int{"foo": 1, "bar": 2, "baz": 3}, [] // map[string]int{"bar": 2} ``` +[[play](https://go.dev/play/p/t1QjCrs-ysk)] + ### OmitByValues Returns same map type filtered by given values. @@ -711,7 +908,9 @@ m := lo.OmitByValues[string, int](map[string]int{"foo": 1, "bar": 2, "baz": 3}, // map[string]int{"bar": 2} ``` -### Entries +[[play](https://go.dev/play/p/9UYZi-hrs8j)] + +### Entries (alias: ToPairs) Transforms a map into array of key/value pairs. @@ -729,7 +928,9 @@ entries := lo.Entries[string, int](map[string]int{"foo": 1, "bar": 2}) // } ``` -### FromEntries +[[play](https://go.dev/play/p/3Dhgx46gawJ)] + +### FromEntries (alias: FromPairs) Transforms an array of key/value pairs into a map. @@ -747,18 +948,22 @@ m := lo.FromEntries[string, int]([]lo.Entry[string, int]{ // map[string]int{"foo": 1, "bar": 2} ``` +[[play](https://go.dev/play/p/oIr5KHFGCEN)] + ### Invert Creates a map composed of the inverted keys and values. If map contains duplicate values, subsequent values overwrite property assignments of previous values. ```go -m1 := lo.Invert[string, int]([map[string]int{"a": 1, "b": 2}) +m1 := lo.Invert[string, int](map[string]int{"a": 1, "b": 2}) // map[int]string{1: "a", 2: "b"} -m2 := lo.Invert[string, int]([map[string]int{"a": 1, "b": 2, "c": 1}) +m2 := lo.Invert[string, int](map[string]int{"a": 1, "b": 2, "c": 1}) // map[int]string{1: "c", 2: "b"} ``` +[[play](https://go.dev/play/p/rFQ4rak6iA1)] + ### Assign Merges multiple maps from left to right. @@ -771,6 +976,8 @@ mergedMaps := lo.Assign[string, int]( // map[string]int{"a": 1, "b": 3, "c": 4} ``` +[[play](https://go.dev/play/p/VhwfJOyxf5o)] + ### MapKeys Manipulates a map keys and transforms it to a map of another type. @@ -782,6 +989,8 @@ m2 := lo.MapKeys[int, int, string](map[int]int{1: 1, 2: 2, 3: 3, 4: 4}, func(_ i // map[string]int{"1": 1, "2": 2, "3": 3, "4": 4} ``` +[[play](https://go.dev/play/p/9_4WPIqOetJ)] + ### MapValues Manipulates a map values and transforms it to a map of another type. @@ -795,6 +1004,23 @@ m2 := lo.MapValues[int, int64, string](m1, func(x int64, _ int) string { // map[int]string{1: "1", 2: "2", 3: "3"} ``` +[[play](https://go.dev/play/p/T_8xAfvcf0W)] + +### MapToSlice + +Transforms a map into a slice based on specific iteratee. + +```go +m := map[int]int64{1: 4, 2: 5, 3: 6} + +s := lo.MapToSlice(m, func(k int, v int64) string { + return fmt.Sprintf("%d_%d", k, v) +}) +// []string{"1_4", "2_5", "3_6"} +``` + +[[play](https://go.dev/play/p/ZuiCZpDt6LD)] + ### Range / RangeFrom / RangeWithSteps Creates an array of numbers (positive and/or negative) progressing from start up to, but not including end. @@ -803,28 +1029,30 @@ Creates an array of numbers (positive and/or negative) progressing from start up result := Range(4) // [0, 1, 2, 3] -result := Range(-4); +result := Range(-4) // [0, -1, -2, -3] -result := RangeFrom(1, 5); -// [1, 2, 3, 4] +result := RangeFrom(1, 5) +// [1, 2, 3, 4, 5] -result := RangeFrom[float64](1.0, 5); -// [1.0, 2.0, 3.0, 4.0] +result := RangeFrom[float64](1.0, 5) +// [1.0, 2.0, 3.0, 4.0, 5.0] -result := RangeWithSteps(0, 20, 5); +result := RangeWithSteps(0, 20, 5) // [0, 5, 10, 15] -result := RangeWithSteps[float32](-1.0, -4.0, -1.0); +result := RangeWithSteps[float32](-1.0, -4.0, -1.0) // [-1.0, -2.0, -3.0] -result := RangeWithSteps(1, 4, -1); +result := RangeWithSteps(1, 4, -1) // [] -result := Range(0); +result := Range(0) // [] ``` +[[play](https://go.dev/play/p/0r6VimXAi9H)] + ### Clamp Clamps number within the inclusive lower and upper bounds. @@ -840,7 +1068,9 @@ r3 := lo.Clamp(42, -10, 10) // 10 ``` -### SumBy +[[play](https://go.dev/play/p/RU4lJNC2hlI)] + +### SumBy Summarizes the values in a collection using the given return value from the iteration function. If collection is empty 0 is returned. @@ -853,6 +1083,8 @@ sum := lo.SumBy(strings, func(item string) int { // 6 ``` +[[play](https://go.dev/play/p/Dz_a_7jN_ca)] + ### Substring Return part of a string. @@ -868,6 +1100,28 @@ sub := lo.Substring("hello", -2, math.MaxUint) // "lo" ``` +[[play](https://go.dev/play/p/TQlxQi82Lu1)] + +### ChunkString + +Returns an array of strings split into groups the length of size. If array can't be split evenly, the final chunk will be the remaining elements. + +```go +lo.ChunkString("123456", 2) +// []string{"12", "34", "56"} + +lo.ChunkString("1234567", 2) +// []string{"12", "34", "56", "7"} + +lo.ChunkString("", 2) +// []string{""} + +lo.ChunkString("1", 2) +// []string{"1"} +``` + +[[play](https://go.dev/play/p/__FLTuJVz54)] + ### RuneLength An alias to utf8.RuneCountInString which returns the number of runes in string. @@ -880,19 +1134,23 @@ sub := len("hellô") // 6 ``` +[[play](https://go.dev/play/p/tuhgW_lWY8l)] + ### T2 -> T9 Creates a tuple from a list of values. ```go -tuple1 := lo.T2[string, int]("x", 1) +tuple1 := lo.T2("x", 1) // Tuple2[string, int]{A: "x", B: 1} func example() (string, int) { return "y", 2 } -tuple2 := lo.T2[string, int](example()) +tuple2 := lo.T2(example()) // Tuple2[string, int]{A: "y", B: 2} ``` +[[play](https://go.dev/play/p/IllL3ZO4BQm)] + ### Unpack2 -> Unpack9 Returns values contained in tuple. @@ -902,6 +1160,8 @@ r1, r2 := lo.Unpack2[string, int](lo.Tuple2[string, int]{"a", 1}) // "a", 1 ``` +[[play](https://go.dev/play/p/xVP_k0kJ96W)] + ### Zip2 -> Zip9 Zip creates a slice of grouped elements, the first of which contains the first elements of the given arrays, the second of which contains the second elements of the given arrays, and so on. @@ -913,6 +1173,8 @@ tuples := lo.Zip2[string, int]([]string{"a", "b"}, []int{1, 2}) // []Tuple2[string, int]{{A: "a", B: 1}, {A: "b", B: 2}} ``` +[[play](https://go.dev/play/p/jujaA6GaJTp)] + ### Unzip2 -> Unzip9 Unzip accepts an array of grouped elements and creates an array regrouping the elements to their pre-zip configuration. @@ -923,6 +1185,235 @@ a, b := lo.Unzip2[string, int]([]Tuple2[string, int]{{A: "a", B: 1}, {A: "b", B: // []int{1, 2} ``` +[[play](https://go.dev/play/p/ciHugugvaAW)] + +### ChannelDispatcher + +Distributes messages from input channels into N child channels. Close events are propagated to children. + +Underlying channels can have a fixed buffer capacity or be unbuffered when cap is 0. + +```go +ch := make(chan int, 42) +for i := 0; i <= 10; i++ { + ch <- i +} + +children := lo.ChannelDispatcher(ch, 5, 10, DispatchingStrategyRoundRobin[int]) +// []<-chan int{...} + +consumer := func(c <-chan int) { + for { + msg, ok := <-c + if !ok { + println("closed") + break + } + + println(msg) + } +} + +for i := range children { + go consumer(children[i]) +} +``` + +Many distributions strategies are available: + +- [lo.DispatchingStrategyRoundRobin](./channel.go): Distributes messages in a rotating sequential manner. +- [lo.DispatchingStrategyRandom](./channel.go): Distributes messages in a random manner. +- [lo.DispatchingStrategyWeightedRandom](./channel.go): Distributes messages in a weighted manner. +- [lo.DispatchingStrategyFirst](./channel.go): Distributes messages in the first non-full channel. +- [lo.DispatchingStrategyLeast](./channel.go): Distributes messages in the emptiest channel. +- [lo.DispatchingStrategyMost](./channel.go): Distributes to the fulliest channel. + +Some strategies bring fallback, in order to favor non-blocking behaviors. See implementations. + +For custom strategies, just implement the `lo.DispatchingStrategy` prototype: + +```go +type DispatchingStrategy[T any] func(message T, messageIndex uint64, channels []<-chan T) int +``` + +Eg: + +```go +type Message struct { + TenantID uuid.UUID +} + +func hash(id uuid.UUID) int { + h := fnv.New32a() + h.Write([]byte(id.String())) + return int(h.Sum32()) +} + +// Routes messages per TenantID. +customStrategy := func(message pubsub.AMQPSubMessage, messageIndex uint64, channels []<-chan pubsub.AMQPSubMessage) int { + destination := hash(message.TenantID) % len(channels) + + // check if channel is full + if len(channels[destination]) < cap(channels[destination]) { + return destination + } + + // fallback when child channel is full + return utils.DispatchingStrategyRoundRobin(message, uint64(destination), channels) +} + +children := lo.ChannelDispatcher(ch, 5, 10, customStrategy) +... +``` + +### SliceToChannel + +Returns a read-only channels of collection elements. Channel is closed after last element. Channel capacity can be customized. + +```go +list := []int{1, 2, 3, 4, 5} + +for v := range lo.SliceToChannel(2, list) { + println(v) +} +// prints 1, then 2, then 3, then 4, then 5 +``` + +### Generator + +Implements the generator design pattern. Channel is closed after last element. Channel capacity can be customized. + +```go +generator := func(yield func(int)) { + yield(1) + yield(2) + yield(3) +} + +for v := range lo.Generator(2, generator) { + println(v) +} +// prints 1, then 2, then 3 +``` + +### Batch + +Creates a slice of n elements from a channel. Returns the slice, the slice length, the read time and the channel status (opened/closed). + +```go +ch := lo.SliceToChannel(2, []int{1, 2, 3, 4, 5}) + +items1, length1, duration1, ok1 := lo.Batch(ch, 3) +// []int{1, 2, 3}, 3, 0s, true +items2, length2, duration2, ok2 := lo.Batch(ch, 3) +// []int{4, 5}, 2, 0s, false +``` + +Example: RabbitMQ consumer 👇 + +```go +ch := readFromQueue() + +for { + // read 1k items + items, length, _, ok := lo.Batch(ch, 1000) + + // do batching stuff + + if !ok { + break + } +} +``` + +### BatchWithTimeout + +Creates a slice of n elements from a channel, with timeout. Returns the slice, the slice length, the read time and the channel status (opened/closed). + +```go +generator := func(yield func(int)) { + for i := 0; i < 5; i++ { + yield(i) + time.Sleep(35*time.Millisecond) + } +} + +ch := lo.Generator(0, generator) + +items1, length1, duration1, ok1 := lo.BatchWithTimeout(ch, 3, 100*time.Millisecond) +// []int{1, 2}, 2, 100ms, true +items2, length2, duration2, ok2 := lo.BatchWithTimeout(ch, 3, 100*time.Millisecond) +// []int{3, 4, 5}, 3, 75ms, true +items3, length3, duration2, ok3 := lo.BatchWithTimeout(ch, 3, 100*time.Millisecond) +// []int{}, 0, 10ms, false +``` + +Example: RabbitMQ consumer 👇 + +```go +ch := readFromQueue() + +for { + // read 1k items + // wait up to 1 second + items, length, _, ok := lo.BatchWithTimeout(ch, 1000, 1*time.Second) + + // do batching stuff + + if !ok { + break + } +} +``` + +Example: Multithreaded RabbitMQ consumer 👇 + +```go +ch := readFromQueue() + +// 5 workers +// prefetch 1k messages per worker +children := lo.ChannelDispatcher(ch, 5, 1000, DispatchingStrategyFirst[int]) + +consumer := func(c <-chan int) { + for { + // read 1k items + // wait up to 1 second + items, length, _, ok := lo.BatchWithTimeout(ch, 1000, 1*time.Second) + + // do batching stuff + + if !ok { + break + } + } +} + +for i := range children { + go consumer(children[i]) +} +``` + +### Contains + +Returns true if an element is present in a collection. + +```go +present := lo.Contains[int]([]int{0, 1, 2, 3, 4, 5}, 5) +// true +``` + +### ContainsBy + +Returns true if the predicate function returns `true`. + +```go +present := lo.ContainsBy[int]([]int{0, 1, 2, 3, 4, 5}, func(x int) bool { + return x == 3 +}) +// true +``` + ### Every Returns true if all elements of a subset are contained into a collection or if the subset is empty. @@ -961,7 +1452,7 @@ ok := lo.Some[int]([]int{0, 1, 2, 3, 4, 5}, []int{-1, 6}) ### SomeBy -Returns true if the predicate returns true for any of the elements in the collection. +Returns true if the predicate returns true for any of the elements in the collection. If the collection is empty SomeBy returns false. ```go @@ -1032,6 +1523,27 @@ union := lo.Union[int]([]int{0, 1, 2, 3, 4, 5}, []int{0, 2, 10}) // []int{0, 1, 2, 3, 4, 5, 10} ``` +### Without + +Returns slice excluding all given values. + +```go +subset := lo.Without[int]([]int{0, 2, 10}, 2) +// []int{0, 10} + +subset := lo.Without[int]([]int{0, 2, 10}, 0, 1, 2, 3, 4, 5) +// []int{10} +``` + +### WithoutEmpty + +Returns slice excluding empty values. + +```go +subset := lo.WithoutEmpty[int]([]int{0, 2, 10}) +// []int{2, 10} +``` + ### IndexOf Returns the index at which the first occurrence of a value is found in an array or return -1 if the value cannot be found. @@ -1044,7 +1556,7 @@ notFound := lo.IndexOf[int]([]int{0, 1, 2, 1, 2, 3}, 6) // -1 ``` -### LastIndex +### LastIndexOf Returns the index at which the last occurrence of a value is found in an array or return -1 if the value cannot be found. @@ -1104,6 +1616,80 @@ str, index, ok := lo.FindLastIndexOf[string]([]string{"foobar"}, func(i string) // "", -1, false ``` +### FindKey + +Returns the key of the first value matching. + +```go +result1, ok1 := lo.FindKey(map[string]int{"foo": 1, "bar": 2, "baz": 3}, 2) +// "bar", true + +result2, ok2 := lo.FindKey(map[string]int{"foo": 1, "bar": 2, "baz": 3}, 42) +// "", false + +type test struct { + foobar string +} +result3, ok3 := lo.FindKey(map[string]test{"foo": test{"foo"}, "bar": test{"bar"}, "baz": test{"baz"}}, test{"foo"}) +// "foo", true +``` + +### FindKeyBy + +Returns the key of the first element predicate returns truthy for. + +```go +result1, ok1 := lo.FindKeyBy(map[string]int{"foo": 1, "bar": 2, "baz": 3}, func(k string, v int) bool { + return k == "foo" +}) +// "foo", true + +result2, ok2 := lo.FindKeyBy(map[string]int{"foo": 1, "bar": 2, "baz": 3}, func(k string, v int) bool { + return false +}) +// "", false +``` + +### FindUniques + +Returns a slice with all the unique elements of the collection. The order of result values is determined by the order they occur in the array. + +```go +uniqueValues := lo.FindUniques[int]([]int{1, 2, 2, 1, 2, 3}) +// []int{3} +``` + +### FindUniquesBy + +Returns a slice with all the unique elements of the collection. The order of result values is determined by the order they occur in the array. It accepts `iteratee` which is invoked for each element in array to generate the criterion by which uniqueness is computed. + +```go +uniqueValues := lo.FindUniquesBy[int, int]([]int{3, 4, 5, 6, 7}, func(i int) int { + return i%3 +}) +// []int{5} +``` + +### FindDuplicates + +Returns a slice with the first occurence of each duplicated elements of the collection. The order of result values is determined by the order they occur in the array. + +```go +duplicatedValues := lo.FindDuplicates[int]([]int{1, 2, 2, 1, 2, 3}) +// []int{1, 2} +``` + +### FindDuplicatesBy + +Returns a slice with the first occurence of each duplicated elements of the collection. The order of result values is determined by the order they occur in the array. It accepts `iteratee` which is invoked for each element in array to generate the criterion by which uniqueness is computed. + +```go +duplicatedValues := lo.FindDuplicatesBy[int, int]([]int{3, 4, 5, 6, 7}, func(i int) int { + return i%3 +}) +// []int{3, 4} +``` + ### Min Search the minimum value of a collection. @@ -1216,6 +1802,31 @@ result := lo.Ternary[string](false, "a", "b") // "b" ``` +[[play](https://go.dev/play/p/t-D7WBL44h2)] + +### TernaryF + +A 1 line if/else statement whose options are functions. + +```go +result := lo.TernaryF[string](true, func() string { return "a" }, func() string { return "b" }) +// "a" + +result := lo.TernaryF[string](false, func() string { return "a" }, func() string { return "b" }) +// "b" +``` + +Useful to avoid nil-pointer dereferencing in intializations, or avoid running unnecessary code + +```go +var s *string + +someStr := TernaryF[string](s == nil, func() string { return uuid.New().String() }, func() string { return *s }) +// ef782193-c30c-4e2e-a7ae-f8ab5e125e02 +``` + +[[play](https://go.dev/play/p/AO4VW20JoqM)] + ### If / ElseIf / Else ```go @@ -1260,6 +1871,8 @@ result := lo.IfF[int](true, func () int { // 1 ``` +[[play](https://go.dev/play/p/WSw3ApMxhyW)] + ### Switch / Case / Default ```go @@ -1309,6 +1922,8 @@ result := lo.Switch[int, string](1). // "1" ``` +[[play](https://go.dev/play/p/TGbKUMAeRUd)] + ### ToPtr Returns a pointer copy of value. @@ -1318,6 +1933,32 @@ ptr := lo.ToPtr[string]("hello world") // *string{"hello world"} ``` +### FromPtr + +Returns the pointer value or empty. + +```go +str := "hello world" +value := lo.FromPtr[string](&str) +// "hello world" + +value := lo.FromPtr[string](nil) +// "" +``` + +### FromPtrOr + +Returns the pointer value or the fallback value. + +```go +str := "hello world" +value := lo.FromPtrOr[string](&str, "empty") +// "hello world" + +value := lo.FromPtrOr[string](nil, "empty") +// "empty" +``` + ### ToSlicePtr Returns a slice of pointer copy of value. @@ -1327,6 +1968,27 @@ ptr := lo.ToSlicePtr[string]([]string{"hello", "world"}) // []*string{"hello", "world"} ``` +### ToAnySlice + +Returns a slice with all elements mapped to `any` type. + +```go +elements := lo.ToAnySlice[int]([]int{1, 5, 1}) +// []any{1, 5, 1} +``` + +### FromAnySlice + +Returns an `any` slice with all elements mapped to a type. Returns false in case of type conversion failure. + +```go +elements, ok := lo.FromAnySlice[string]([]any{"foobar", 42}) +// []string{}, false + +elements, ok := lo.FromAnySlice[string]([]any{"foobar", "42"}) +// []string{"foobar", "42"}, true +``` + ### Empty Returns an empty value. @@ -1340,6 +2002,56 @@ lo.Empty[bool]() // false ``` +### IsEmpty + +Returns true if argument is a zero value. + +```go +lo.IsEmpty[int](0) +// true +lo.IsEmpty[int](42) +// false + +lo.IsEmpty[string]("") +// true +lo.IsEmpty[bool]("foobar") +// false + +type test struct { + foobar string +} + +lo.IsEmpty[test](test{foobar: ""}) +// true +lo.IsEmpty[test](test{foobar: "foobar"}) +// false +``` + +### IsNotEmpty + +Returns true if argument is a zero value. + +```go +lo.IsNotEmpty[int](0) +// false +lo.IsNotEmpty[int](42) +// true + +lo.IsNotEmpty[string]("") +// false +lo.IsNotEmpty[bool]("foobar") +// true + +type test struct { + foobar string +} + +lo.IsNotEmpty[test](test{foobar: ""}) +// false +lo.IsNotEmpty[test](test{foobar: "foobar"}) +// true +``` + ### Coalesce Returns the first non-empty arguments. Arguments must be comparable. @@ -1357,9 +2069,24 @@ result, ok := lo.Coalesce[*string](nil, nilStr, &str) // &"foobar" true ``` +### Partial + +Returns new function that, when called, has its first argument set to the provided value. + +```go +add := func(x, y int) int { return x + y } +f := lo.Partial(add, 5) + +f(10) +// 15 + +f(42) +// 47 +``` + ### Attempt -Invokes a function N times until it returns valid output. Returning either the caught error or nil. When first argument is less than `1`, the function runs until a sucessfull response is returned. +Invokes a function N times until it returns valid output. Returning either the caught error or nil. When first argument is less than `1`, the function runs until a successful response is returned. ```go iter, err := lo.Attempt(42, func(i int) error { @@ -1395,11 +2122,13 @@ iter, err := lo.Attempt(0, func(i int) error { For more advanced retry strategies (delay, exponential backoff...), please take a look on [cenkalti/backoff](https://github.com/cenkalti/backoff). +[[play](https://go.dev/play/p/3ggJZ2ZKcMj)] + ### AttemptWithDelay -Invokes a function N times until it returns valid output, with a pause betwwen each call. Returning either the caught error or nil. +Invokes a function N times until it returns valid output, with a pause between each call. Returning either the caught error or nil. -When first argument is less than `1`, the function runs until a sucessfull response is returned. +When first argument is less than `1`, the function runs until a successful response is returned. ```go iter, duration, err := lo.AttemptWithDelay(5, 2*time.Second, func(i int, duration time.Duration) error { @@ -1416,6 +2145,8 @@ iter, duration, err := lo.AttemptWithDelay(5, 2*time.Second, func(i int, duratio For more advanced retry strategies (delay, exponential backoff...), please take a look on [cenkalti/backoff](https://github.com/cenkalti/backoff). +[[play](https://go.dev/play/p/tVs6CygC7m1)] + ### Debounce `NewDebounce` creates a debounced instance that delays invoking functions given until after wait milliseconds have elapsed, until `cancel` is called. @@ -1434,6 +2165,8 @@ time.Sleep(1 * time.Second) cancel() ``` +[[play](https://go.dev/play/p/mz32VMK2nqe)] + ### Synchronize Wraps the underlying callback in a mutex. It receives an optional mutex. @@ -1493,6 +2226,22 @@ ch := lo.Async2(func() (int, string) { // chan lo.Tuple2[int, string] ({42, "Hello"}) ``` +### Validate + +Helper function that creates an error when a condition is not met. + +```go +slice := []string{"a"} +val := lo.Validate(len(slice) == 0, "Slice should be empty but contains %v", slice) +// error("Slice should be empty but contains [a]") + +slice := []string{} +val := lo.Validate(len(slice) == 0, "Slice should be empty but contains %v", slice) +// nil +``` + +[[play](https://go.dev/play/p/vPyh51XpCBt)] + ### Must Wraps a function call to panics if second argument is `error` or `false`, returns the value otherwise. @@ -1505,8 +2254,12 @@ val := lo.Must(time.Parse("2006-01-02", "bad-value")) // panics ``` +[[play](https://go.dev/play/p/TMoWrRp3DyC)] + ### Must{0->6} -Must* has the same behavior than Must, but returns multiple values. + +Must\* has the same behavior than Must, but returns multiple values. + ```go func example0() (error) func example1() (int, error) @@ -1525,7 +2278,8 @@ val1, val2, val3, val4, val5 := lo.Must5(example5()) val1, val2, val3, val4, val5, val6 := lo.Must6(example6()) ``` -You can wrap functions like `func (...) (..., ok bool)`. +You can wrap functions like `func (...) (..., ok bool)`. + ```go // math.Signbit(float64) bool lo.Must0(math.Signbit(v)) @@ -1534,7 +2288,23 @@ lo.Must0(math.Signbit(v)) before, after := lo.Must2(bytes.Cut(s, sep)) ``` -## Try +You can give context to the panic message by adding some printf-like arguments. + +```go +val, ok := lo.Find(myString, func(i string) bool { + return i == requiredChar +}) +lo.Must0(ok, "'%s' must always contain '%s'", myString, requiredChar) + +list := []int{0, 1, 2} +item := 5 +lo.Must0(lo.Contains[int](list, item), "'%s' must always contain '%s'", list, item) +... +``` + +[[play](https://go.dev/play/p/TMoWrRp3DyC)] + +### Try Calls the function and return false in case of error and on panic. @@ -1556,7 +2326,9 @@ ok := lo.Try(func() error { // false ``` -## Try{0->6} +[[play](https://go.dev/play/p/mTyyWUvn9u4)] + +### Try{0->6} The same behavior than `Try`, but callback returns 2 variables. @@ -1568,7 +2340,52 @@ ok := lo.Try2(func() (string, error) { // false ``` -## TryWithErrorValue +[[play](https://go.dev/play/p/mTyyWUvn9u4)] + +### TryOr + +Calls the function and return a default value in case of error and on panic. + +```go +str, ok := lo.TryOr(func() (string, error) { + panic("error") + return "hello", nil +}, "world") +// world +// false + +ok := lo.TryOr(func() error { + return "hello", nil +}, "world") +// hello +// true + +ok := lo.TryOr(func() error { + return "hello", fmt.Errorf("error") +}, "world") +// world +// false +``` + +[[play](https://go.dev/play/p/B4F7Wg2Zh9X)] + +### TryOr{0->6} + +The same behavior than `TryOr`, but callback returns 2 variables. + +```go +str, nbr, ok := lo.TryOr2(func() (string, int, error) { + panic("error") + return "hello", 42, nil +}, "world", 21) +// world +// 21 +// false +``` + +[[play](https://go.dev/play/p/B4F7Wg2Zh9X)] + +### TryWithErrorValue The same behavior than `Try`, but also returns value passed to panic. @@ -1580,7 +2397,9 @@ err, ok := lo.TryWithErrorValue(func() error { // "error", false ``` -## TryCatch +[[play](https://go.dev/play/p/Kc7afQIT2Fs)] + +### TryCatch The same behavior than `Try`, but calls the catch function in case of error. @@ -1597,7 +2416,9 @@ ok := lo.TryCatch(func() error { // caught == true ``` -## TryCatchWithErrorValue +[[play](https://go.dev/play/p/PnOON-EqBiU)] + +### TryCatchWithErrorValue The same behavior than `TryWithErrorValue`, but calls the catch function in case of error. @@ -1614,6 +2435,33 @@ ok := lo.TryCatchWithErrorValue(func() error { // caught == true ``` +[[play](https://go.dev/play/p/8Pc9gwX_GZO)] + +### ErrorsAs + +A shortcut for: + +```go +err := doSomething() + +var rateLimitErr *RateLimitError +if ok := errors.As(err, &rateLimitErr); ok { + // retry later +} +``` + +1 line `lo` helper: + +```go +err := doSomething() + +if rateLimitErr, ok := lo.ErrorsAs[*RateLimitError](err); ok { + // retry later +} +``` + +[[play](https://go.dev/play/p/8wk5rH8UfrE)] + ## 🛩 Benchmark We executed a simple benchmark with the a dead-simple `lo.Map` loop: @@ -1645,10 +2493,10 @@ PASS ok github.com/samber/lo 6.657s ``` -- `lo.Map` is way faster (x7) than `go-funk`, a relection-based Map implementation. +- `lo.Map` is way faster (x7) than `go-funk`, a reflection-based Map implementation. - `lo.Map` have the same allocation profile than `for`. - `lo.Map` is 4% slower than `for`. -- `lop.Map` is slower than `lo.Map` because it implies more memory allocation and locks. `lop.Map` will be usefull for long-running callbacks, such as i/o bound processing. +- `lop.Map` is slower than `lo.Map` because it implies more memory allocation and locks. `lop.Map` will be useful for long-running callbacks, such as i/o bound processing. - `for` beats other implementations for memory and CPU. ## 🤝 Contributing @@ -1659,14 +2507,6 @@ ok github.com/samber/lo 6.657s Don't hesitate ;) -### Install go 1.18 - -```bash -make go1.18beta1 -``` - -If your OS currently not default to Go 1.18, replace `BIN=go` by `BIN=go1.18beta1` in the Makefile. - ### With Docker ```bash diff --git a/vendor/github.com/samber/lo/channel.go b/vendor/github.com/samber/lo/channel.go new file mode 100644 index 00000000..ccbd3602 --- /dev/null +++ b/vendor/github.com/samber/lo/channel.go @@ -0,0 +1,228 @@ +package lo + +import ( + "math/rand" + "time" +) + +type DispatchingStrategy[T any] func(msg T, index uint64, channels []<-chan T) int + +// ChannelDispatcher distributes messages from input channels into N child channels. +// Close events are propagated to children. +// Underlying channels can have a fixed buffer capacity or be unbuffered when cap is 0. +func ChannelDispatcher[T any](stream <-chan T, count int, channelBufferCap int, strategy DispatchingStrategy[T]) []<-chan T { + children := createChannels[T](count, channelBufferCap) + + roChildren := channelsToReadOnly(children) + + go func() { + // propagate channel closing to children + defer closeChannels(children) + + var i uint64 = 0 + + for { + msg, ok := <-stream + if !ok { + return + } + + destination := strategy(msg, i, roChildren) % count + children[destination] <- msg + + i++ + } + }() + + return roChildren +} + +func createChannels[T any](count int, channelBufferCap int) []chan T { + children := make([]chan T, 0, count) + + for i := 0; i < count; i++ { + children = append(children, make(chan T, channelBufferCap)) + } + + return children +} + +func channelsToReadOnly[T any](children []chan T) []<-chan T { + roChildren := make([]<-chan T, 0, len(children)) + + for i := range children { + roChildren = append(roChildren, children[i]) + } + + return roChildren +} + +func closeChannels[T any](children []chan T) { + for i := 0; i < len(children); i++ { + close(children[i]) + } +} + +func channelIsNotFull[T any](ch <-chan T) bool { + return cap(ch) == 0 || len(ch) < cap(ch) +} + +// DispatchingStrategyRoundRobin distributes messages in a rotating sequential manner. +// If the channel capacity is exceeded, the next channel will be selected and so on. +func DispatchingStrategyRoundRobin[T any](msg T, index uint64, channels []<-chan T) int { + for { + i := int(index % uint64(len(channels))) + if channelIsNotFull(channels[i]) { + return i + } + + index++ + time.Sleep(10 * time.Microsecond) // prevent CPU from burning 🔥 + } +} + +// DispatchingStrategyRandom distributes messages in a random manner. +// If the channel capacity is exceeded, another random channel will be selected and so on. +func DispatchingStrategyRandom[T any](msg T, index uint64, channels []<-chan T) int { + for { + i := rand.Intn(len(channels)) + if channelIsNotFull(channels[i]) { + return i + } + + time.Sleep(10 * time.Microsecond) // prevent CPU from burning 🔥 + } +} + +// DispatchingStrategyRandom distributes messages in a weighted manner. +// If the channel capacity is exceeded, another random channel will be selected and so on. +func DispatchingStrategyWeightedRandom[T any](weights []int) DispatchingStrategy[T] { + seq := []int{} + + for i := 0; i < len(weights); i++ { + for j := 0; j < weights[i]; j++ { + seq = append(seq, i) + } + } + + return func(msg T, index uint64, channels []<-chan T) int { + for { + i := seq[rand.Intn(len(seq))] + if channelIsNotFull(channels[i]) { + return i + } + + time.Sleep(10 * time.Microsecond) // prevent CPU from burning 🔥 + } + } +} + +// DispatchingStrategyFirst distributes messages in the first non-full channel. +// If the capacity of the first channel is exceeded, the second channel will be selected and so on. +func DispatchingStrategyFirst[T any](msg T, index uint64, channels []<-chan T) int { + for { + for i := range channels { + if channelIsNotFull(channels[i]) { + return i + } + } + + time.Sleep(10 * time.Microsecond) // prevent CPU from burning 🔥 + } +} + +// DispatchingStrategyLeast distributes messages in the emptiest channel. +func DispatchingStrategyLeast[T any](msg T, index uint64, channels []<-chan T) int { + seq := Range(len(channels)) + + return MinBy(seq, func(item int, min int) bool { + return len(channels[item]) < len(channels[min]) + }) +} + +// DispatchingStrategyMost distributes messages in the fulliest channel. +// If the channel capacity is exceeded, the next channel will be selected and so on. +func DispatchingStrategyMost[T any](msg T, index uint64, channels []<-chan T) int { + seq := Range(len(channels)) + + return MaxBy(seq, func(item int, max int) bool { + return len(channels[item]) > len(channels[max]) && channelIsNotFull(channels[item]) + }) +} + +// SliceToChannel returns a read-only channels of collection elements. +func SliceToChannel[T any](bufferSize int, collection []T) <-chan T { + ch := make(chan T, bufferSize) + + go func() { + for _, item := range collection { + ch <- item + } + + close(ch) + }() + + return ch +} + +// Generator implements the generator design pattern. +func Generator[T any](bufferSize int, generator func(yield func(T))) <-chan T { + ch := make(chan T, bufferSize) + + go func() { + // WARNING: infinite loop + generator(func(t T) { + ch <- t + }) + + close(ch) + }() + + return ch +} + +// Batch creates a slice of n elements from a channel. Returns the slice and the slice length. +// @TODO: we should probaby provide an helper that reuse the same buffer. +func Batch[T any](ch <-chan T, size int) (collection []T, length int, readTime time.Duration, ok bool) { + buffer := make([]T, 0, size) + index := 0 + now := time.Now() + + for ; index < size; index++ { + item, ok := <-ch + if !ok { + return buffer, index, time.Since(now), false + } + + buffer = append(buffer, item) + } + + return buffer, index, time.Since(now), true +} + +// BatchWithTimeout creates a slice of n elements from a channel, with timeout. Returns the slice and the slice length. +// @TODO: we should probaby provide an helper that reuse the same buffer. +func BatchWithTimeout[T any](ch <-chan T, size int, timeout time.Duration) (collection []T, length int, readTime time.Duration, ok bool) { + expire := time.NewTimer(timeout) + defer expire.Stop() + + buffer := make([]T, 0, size) + index := 0 + now := time.Now() + + for ; index < size; index++ { + select { + case item, ok := <-ch: + if !ok { + return buffer, index, time.Since(now), false + } + + buffer = append(buffer, item) + + case <-expire.C: + return buffer, index, time.Since(now), true + } + } + + return buffer, index, time.Since(now), true +} diff --git a/vendor/github.com/samber/lo/condition.go b/vendor/github.com/samber/lo/condition.go index 6fd31f1e..1d4e75d2 100644 --- a/vendor/github.com/samber/lo/condition.go +++ b/vendor/github.com/samber/lo/condition.go @@ -1,6 +1,7 @@ package lo // Ternary is a 1 line if/else statement. +// Play: https://go.dev/play/p/t-D7WBL44h2 func Ternary[T any](condition bool, ifOutput T, elseOutput T) T { if condition { return ifOutput @@ -9,12 +10,23 @@ func Ternary[T any](condition bool, ifOutput T, elseOutput T) T { return elseOutput } +// TernaryF is a 1 line if/else statement whose options are functions +// Play: https://go.dev/play/p/AO4VW20JoqM +func TernaryF[T any](condition bool, ifFunc func() T, elseFunc func() T) T { + if condition { + return ifFunc() + } + + return elseFunc() +} + type ifElse[T any] struct { result T done bool } // If. +// Play: https://go.dev/play/p/WSw3ApMxhyW func If[T any](condition bool, result T) *ifElse[T] { if condition { return &ifElse[T]{result, true} @@ -25,6 +37,7 @@ func If[T any](condition bool, result T) *ifElse[T] { } // IfF. +// Play: https://go.dev/play/p/WSw3ApMxhyW func IfF[T any](condition bool, resultF func() T) *ifElse[T] { if condition { return &ifElse[T]{resultF(), true} @@ -35,6 +48,7 @@ func IfF[T any](condition bool, resultF func() T) *ifElse[T] { } // ElseIf. +// Play: https://go.dev/play/p/WSw3ApMxhyW func (i *ifElse[T]) ElseIf(condition bool, result T) *ifElse[T] { if !i.done && condition { i.result = result @@ -45,6 +59,7 @@ func (i *ifElse[T]) ElseIf(condition bool, result T) *ifElse[T] { } // ElseIfF. +// Play: https://go.dev/play/p/WSw3ApMxhyW func (i *ifElse[T]) ElseIfF(condition bool, resultF func() T) *ifElse[T] { if !i.done && condition { i.result = resultF() @@ -55,6 +70,7 @@ func (i *ifElse[T]) ElseIfF(condition bool, resultF func() T) *ifElse[T] { } // Else. +// Play: https://go.dev/play/p/WSw3ApMxhyW func (i *ifElse[T]) Else(result T) T { if i.done { return i.result @@ -64,6 +80,7 @@ func (i *ifElse[T]) Else(result T) T { } // ElseF. +// Play: https://go.dev/play/p/WSw3ApMxhyW func (i *ifElse[T]) ElseF(resultF func() T) T { if i.done { return i.result @@ -79,6 +96,7 @@ type switchCase[T comparable, R any] struct { } // Switch is a pure functional switch/case/default statement. +// Play: https://go.dev/play/p/TGbKUMAeRUd func Switch[T comparable, R any](predicate T) *switchCase[T, R] { var result R @@ -90,6 +108,7 @@ func Switch[T comparable, R any](predicate T) *switchCase[T, R] { } // Case. +// Play: https://go.dev/play/p/TGbKUMAeRUd func (s *switchCase[T, R]) Case(val T, result R) *switchCase[T, R] { if !s.done && s.predicate == val { s.result = result @@ -100,6 +119,7 @@ func (s *switchCase[T, R]) Case(val T, result R) *switchCase[T, R] { } // CaseF. +// Play: https://go.dev/play/p/TGbKUMAeRUd func (s *switchCase[T, R]) CaseF(val T, cb func() R) *switchCase[T, R] { if !s.done && s.predicate == val { s.result = cb() @@ -110,6 +130,7 @@ func (s *switchCase[T, R]) CaseF(val T, cb func() R) *switchCase[T, R] { } // Default. +// Play: https://go.dev/play/p/TGbKUMAeRUd func (s *switchCase[T, R]) Default(result R) R { if !s.done { s.result = result @@ -119,6 +140,7 @@ func (s *switchCase[T, R]) Default(result R) R { } // DefaultF. +// Play: https://go.dev/play/p/TGbKUMAeRUd func (s *switchCase[T, R]) DefaultF(cb func() R) R { if !s.done { s.result = cb() diff --git a/vendor/github.com/samber/lo/docker-compose.yml b/vendor/github.com/samber/lo/docker-compose.yml index c6f3f652..511e85fd 100644 --- a/vendor/github.com/samber/lo/docker-compose.yml +++ b/vendor/github.com/samber/lo/docker-compose.yml @@ -2,8 +2,8 @@ version: '3' services: dev: - build: . + image: golang:1.18-bullseye volumes: - ./:/go/src/github.com/samber/lo working_dir: /go/src/github.com/samber/lo - command: bash -c 'make tools ; make watch-test' + command: make watch-test diff --git a/vendor/github.com/samber/lo/errors.go b/vendor/github.com/samber/lo/errors.go index 0b50ad49..0a4cb5b8 100644 --- a/vendor/github.com/samber/lo/errors.go +++ b/vendor/github.com/samber/lo/errors.go @@ -1,62 +1,115 @@ package lo +import ( + "errors" + "fmt" + "reflect" +) + +// Validate is a helper that creates an error when a condition is not met. +// Play: https://go.dev/play/p/vPyh51XpCBt +func Validate(ok bool, format string, args ...any) error { + if !ok { + return fmt.Errorf(fmt.Sprintf(format, args...)) + } + return nil +} + +func messageFromMsgAndArgs(msgAndArgs ...interface{}) string { + if len(msgAndArgs) == 1 { + if msgAsStr, ok := msgAndArgs[0].(string); ok { + return msgAsStr + } + return fmt.Sprintf("%+v", msgAndArgs[0]) + } + if len(msgAndArgs) > 1 { + return fmt.Sprintf(msgAndArgs[0].(string), msgAndArgs[1:]...) + } + return "" +} + // must panics if err is error or false. -func must(err any) { - b, isBool := err.(bool) - if isBool && !b { - panic("not ok") +func must(err any, messageArgs ...interface{}) { + if err == nil { + return } - e, isError := err.(error) - if isError { - panic(e) + switch e := err.(type) { + case bool: + if !e { + message := messageFromMsgAndArgs(messageArgs...) + if message == "" { + message = "not ok" + } + + panic(message) + } + + case error: + message := messageFromMsgAndArgs(messageArgs...) + if message != "" { + panic(message + ": " + e.Error()) + } else { + panic(e.Error()) + } + + default: + panic("must: invalid err type '" + reflect.TypeOf(err).Name() + "', should either be a bool or an error") } } // Must is a helper that wraps a call to a function returning a value and an error // and panics if err is error or false. -func Must[T any](val T, err any) T { - must(err) +// Play: https://go.dev/play/p/TMoWrRp3DyC +func Must[T any](val T, err any, messageArgs ...interface{}) T { + must(err, messageArgs...) return val } // Must0 has the same behavior than Must, but callback returns no variable. -func Must0(err any) { - must(err) +// Play: https://go.dev/play/p/TMoWrRp3DyC +func Must0(err any, messageArgs ...interface{}) { + must(err, messageArgs...) } // Must1 is an alias to Must -func Must1[T any](val T, err any) T { - return Must(val, err) +// Play: https://go.dev/play/p/TMoWrRp3DyC +func Must1[T any](val T, err any, messageArgs ...interface{}) T { + return Must(val, err, messageArgs...) } // Must2 has the same behavior than Must, but callback returns 2 variables. -func Must2[T1 any, T2 any](val1 T1, val2 T2, err any) (T1, T2) { - must(err) +// Play: https://go.dev/play/p/TMoWrRp3DyC +func Must2[T1 any, T2 any](val1 T1, val2 T2, err any, messageArgs ...interface{}) (T1, T2) { + must(err, messageArgs...) return val1, val2 } // Must3 has the same behavior than Must, but callback returns 3 variables. -func Must3[T1 any, T2 any, T3 any](val1 T1, val2 T2, val3 T3, err any) (T1, T2, T3) { - must(err) +// Play: https://go.dev/play/p/TMoWrRp3DyC +func Must3[T1 any, T2 any, T3 any](val1 T1, val2 T2, val3 T3, err any, messageArgs ...interface{}) (T1, T2, T3) { + must(err, messageArgs...) return val1, val2, val3 } // Must4 has the same behavior than Must, but callback returns 4 variables. -func Must4[T1 any, T2 any, T3 any, T4 any](val1 T1, val2 T2, val3 T3, val4 T4, err any) (T1, T2, T3, T4) { - must(err) +// Play: https://go.dev/play/p/TMoWrRp3DyC +func Must4[T1 any, T2 any, T3 any, T4 any](val1 T1, val2 T2, val3 T3, val4 T4, err any, messageArgs ...interface{}) (T1, T2, T3, T4) { + must(err, messageArgs...) return val1, val2, val3, val4 } // Must5 has the same behavior than Must, but callback returns 5 variables. -func Must5[T1 any, T2 any, T3 any, T4 any, T5 any](val1 T1, val2 T2, val3 T3, val4 T4, val5 T5, err any) (T1, T2, T3, T4, T5) { - must(err) +// Play: https://go.dev/play/p/TMoWrRp3DyC +func Must5[T1 any, T2 any, T3 any, T4 any, T5 any](val1 T1, val2 T2, val3 T3, val4 T4, val5 T5, err any, messageArgs ...interface{}) (T1, T2, T3, T4, T5) { + must(err, messageArgs...) return val1, val2, val3, val4, val5 } // Must6 has the same behavior than Must, but callback returns 6 variables. -func Must6[T1 any, T2 any, T3 any, T4 any, T5 any, T6 any](val1 T1, val2 T2, val3 T3, val4 T4, val5 T5, val6 T6, err any) (T1, T2, T3, T4, T5, T6) { - must(err) +// Play: https://go.dev/play/p/TMoWrRp3DyC +func Must6[T1 any, T2 any, T3 any, T4 any, T5 any, T6 any](val1 T1, val2 T2, val3 T3, val4 T4, val5 T5, val6 T6, err any, messageArgs ...interface{}) (T1, T2, T3, T4, T5, T6) { + must(err, messageArgs...) return val1, val2, val3, val4, val5, val6 } @@ -79,6 +132,7 @@ func Try(callback func() error) (ok bool) { } // Try0 has the same behavior than Try, but callback returns no variable. +// Play: https://go.dev/play/p/mTyyWUvn9u4 func Try0(callback func()) bool { return Try(func() error { callback() @@ -87,11 +141,13 @@ func Try0(callback func()) bool { } // Try1 is an alias to Try. -func Try1[T any](callback func() error) bool { +// Play: https://go.dev/play/p/mTyyWUvn9u4 +func Try1(callback func() error) bool { return Try(callback) } // Try2 has the same behavior than Try, but callback returns 2 variables. +// Play: https://go.dev/play/p/mTyyWUvn9u4 func Try2[T any](callback func() (T, error)) bool { return Try(func() error { _, err := callback() @@ -100,6 +156,7 @@ func Try2[T any](callback func() (T, error)) bool { } // Try3 has the same behavior than Try, but callback returns 3 variables. +// Play: https://go.dev/play/p/mTyyWUvn9u4 func Try3[T, R any](callback func() (T, R, error)) bool { return Try(func() error { _, _, err := callback() @@ -108,6 +165,7 @@ func Try3[T, R any](callback func() (T, R, error)) bool { } // Try4 has the same behavior than Try, but callback returns 4 variables. +// Play: https://go.dev/play/p/mTyyWUvn9u4 func Try4[T, R, S any](callback func() (T, R, S, error)) bool { return Try(func() error { _, _, _, err := callback() @@ -116,6 +174,7 @@ func Try4[T, R, S any](callback func() (T, R, S, error)) bool { } // Try5 has the same behavior than Try, but callback returns 5 variables. +// Play: https://go.dev/play/p/mTyyWUvn9u4 func Try5[T, R, S, Q any](callback func() (T, R, S, Q, error)) bool { return Try(func() error { _, _, _, _, err := callback() @@ -124,6 +183,7 @@ func Try5[T, R, S, Q any](callback func() (T, R, S, Q, error)) bool { } // Try6 has the same behavior than Try, but callback returns 6 variables. +// Play: https://go.dev/play/p/mTyyWUvn9u4 func Try6[T, R, S, Q, U any](callback func() (T, R, S, Q, U, error)) bool { return Try(func() error { _, _, _, _, _, err := callback() @@ -131,7 +191,125 @@ func Try6[T, R, S, Q, U any](callback func() (T, R, S, Q, U, error)) bool { }) } +// TryOr has the same behavior than Must, but returns a default value in case of error. +// Play: https://go.dev/play/p/B4F7Wg2Zh9X +func TryOr[A any](callback func() (A, error), fallbackA A) (A, bool) { + return TryOr1(callback, fallbackA) +} + +// TryOr1 has the same behavior than Must, but returns a default value in case of error. +// Play: https://go.dev/play/p/B4F7Wg2Zh9X +func TryOr1[A any](callback func() (A, error), fallbackA A) (A, bool) { + ok := false + + Try0(func() { + a, err := callback() + if err == nil { + fallbackA = a + ok = true + } + }) + + return fallbackA, ok +} + +// TryOr2 has the same behavior than Must, but returns a default value in case of error. +// Play: https://go.dev/play/p/B4F7Wg2Zh9X +func TryOr2[A any, B any](callback func() (A, B, error), fallbackA A, fallbackB B) (A, B, bool) { + ok := false + + Try0(func() { + a, b, err := callback() + if err == nil { + fallbackA = a + fallbackB = b + ok = true + } + }) + + return fallbackA, fallbackB, ok +} + +// TryOr3 has the same behavior than Must, but returns a default value in case of error. +// Play: https://go.dev/play/p/B4F7Wg2Zh9X +func TryOr3[A any, B any, C any](callback func() (A, B, C, error), fallbackA A, fallbackB B, fallbackC C) (A, B, C, bool) { + ok := false + + Try0(func() { + a, b, c, err := callback() + if err == nil { + fallbackA = a + fallbackB = b + fallbackC = c + ok = true + } + }) + + return fallbackA, fallbackB, fallbackC, ok +} + +// TryOr4 has the same behavior than Must, but returns a default value in case of error. +// Play: https://go.dev/play/p/B4F7Wg2Zh9X +func TryOr4[A any, B any, C any, D any](callback func() (A, B, C, D, error), fallbackA A, fallbackB B, fallbackC C, fallbackD D) (A, B, C, D, bool) { + ok := false + + Try0(func() { + a, b, c, d, err := callback() + if err == nil { + fallbackA = a + fallbackB = b + fallbackC = c + fallbackD = d + ok = true + } + }) + + return fallbackA, fallbackB, fallbackC, fallbackD, ok +} + +// TryOr5 has the same behavior than Must, but returns a default value in case of error. +// Play: https://go.dev/play/p/B4F7Wg2Zh9X +func TryOr5[A any, B any, C any, D any, E any](callback func() (A, B, C, D, E, error), fallbackA A, fallbackB B, fallbackC C, fallbackD D, fallbackE E) (A, B, C, D, E, bool) { + ok := false + + Try0(func() { + a, b, c, d, e, err := callback() + if err == nil { + fallbackA = a + fallbackB = b + fallbackC = c + fallbackD = d + fallbackE = e + ok = true + } + }) + + return fallbackA, fallbackB, fallbackC, fallbackD, fallbackE, ok +} + +// TryOr6 has the same behavior than Must, but returns a default value in case of error. +// Play: https://go.dev/play/p/B4F7Wg2Zh9X +func TryOr6[A any, B any, C any, D any, E any, F any](callback func() (A, B, C, D, E, F, error), fallbackA A, fallbackB B, fallbackC C, fallbackD D, fallbackE E, fallbackF F) (A, B, C, D, E, F, bool) { + ok := false + + Try0(func() { + a, b, c, d, e, f, err := callback() + if err == nil { + fallbackA = a + fallbackB = b + fallbackC = c + fallbackD = d + fallbackE = e + fallbackF = f + ok = true + } + }) + + return fallbackA, fallbackB, fallbackC, fallbackD, fallbackE, fallbackF, ok +} + // TryWithErrorValue has the same behavior than Try, but also returns value passed to panic. +// Play: https://go.dev/play/p/Kc7afQIT2Fs func TryWithErrorValue(callback func() error) (errorValue any, ok bool) { ok = true @@ -152,6 +330,7 @@ func TryWithErrorValue(callback func() error) (errorValue any, ok bool) { } // TryCatch has the same behavior than Try, but calls the catch function in case of error. +// Play: https://go.dev/play/p/PnOON-EqBiU func TryCatch(callback func() error, catch func()) { if !Try(callback) { catch() @@ -159,8 +338,17 @@ func TryCatch(callback func() error, catch func()) { } // TryCatchWithErrorValue has the same behavior than TryWithErrorValue, but calls the catch function in case of error. +// Play: https://go.dev/play/p/8Pc9gwX_GZO func TryCatchWithErrorValue(callback func() error, catch func(any)) { if err, ok := TryWithErrorValue(callback); !ok { catch(err) } } + +// ErrorsAs is a shortcut for errors.As(err, &&T). +// Play: https://go.dev/play/p/8wk5rH8UfrE +func ErrorsAs[T error](err error) (T, bool) { + var t T + ok := errors.As(err, &t) + return t, ok +} diff --git a/vendor/github.com/samber/lo/find.go b/vendor/github.com/samber/lo/find.go index 8f24e238..e40bfcc8 100644 --- a/vendor/github.com/samber/lo/find.go +++ b/vendor/github.com/samber/lo/find.go @@ -2,7 +2,6 @@ package lo import ( "fmt" - "math" "math/rand" "golang.org/x/exp/constraints" @@ -87,6 +86,140 @@ func FindOrElse[T any](collection []T, fallback T, predicate func(T) bool) T { return fallback } +// FindKey returns the key of the first value matching. +func FindKey[K comparable, V comparable](object map[K]V, value V) (K, bool) { + for k, v := range object { + if v == value { + return k, true + } + } + + return Empty[K](), false +} + +// FindKeyBy returns the key of the first element predicate returns truthy for. +func FindKeyBy[K comparable, V any](object map[K]V, predicate func(K, V) bool) (K, bool) { + for k, v := range object { + if predicate(k, v) { + return k, true + } + } + + return Empty[K](), false +} + +// FindUniques returns a slice with all the unique elements of the collection. +// The order of result values is determined by the order they occur in the collection. +func FindUniques[T comparable](collection []T) []T { + isDupl := make(map[T]bool, len(collection)) + + for _, item := range collection { + duplicated, ok := isDupl[item] + if !ok { + isDupl[item] = false + } else if !duplicated { + isDupl[item] = true + } + } + + result := make([]T, 0, len(collection)-len(isDupl)) + + for _, item := range collection { + if duplicated := isDupl[item]; !duplicated { + result = append(result, item) + } + } + + return result +} + +// FindUniquesBy returns a slice with all the unique elements of the collection. +// The order of result values is determined by the order they occur in the array. It accepts `iteratee` which is +// invoked for each element in array to generate the criterion by which uniqueness is computed. +func FindUniquesBy[T any, U comparable](collection []T, iteratee func(T) U) []T { + isDupl := make(map[U]bool, len(collection)) + + for _, item := range collection { + key := iteratee(item) + + duplicated, ok := isDupl[key] + if !ok { + isDupl[key] = false + } else if !duplicated { + isDupl[key] = true + } + } + + result := make([]T, 0, len(collection)-len(isDupl)) + + for _, item := range collection { + key := iteratee(item) + + if duplicated := isDupl[key]; !duplicated { + result = append(result, item) + } + } + + return result +} + +// FindDuplicates returns a slice with the first occurence of each duplicated elements of the collection. +// The order of result values is determined by the order they occur in the collection. +func FindDuplicates[T comparable](collection []T) []T { + isDupl := make(map[T]bool, len(collection)) + + for _, item := range collection { + duplicated, ok := isDupl[item] + if !ok { + isDupl[item] = false + } else if !duplicated { + isDupl[item] = true + } + } + + result := make([]T, 0, len(collection)-len(isDupl)) + + for _, item := range collection { + if duplicated := isDupl[item]; duplicated { + result = append(result, item) + isDupl[item] = false + } + } + + return result +} + +// FindDuplicatesBy returns a slice with the first occurence of each duplicated elements of the collection. +// The order of result values is determined by the order they occur in the array. It accepts `iteratee` which is +// invoked for each element in array to generate the criterion by which uniqueness is computed. +func FindDuplicatesBy[T any, U comparable](collection []T, iteratee func(T) U) []T { + isDupl := make(map[U]bool, len(collection)) + + for _, item := range collection { + key := iteratee(item) + + duplicated, ok := isDupl[key] + if !ok { + isDupl[key] = false + } else if !duplicated { + isDupl[key] = true + } + } + + result := make([]T, 0, len(collection)-len(isDupl)) + + for _, item := range collection { + key := iteratee(item) + + if duplicated := isDupl[key]; duplicated { + result = append(result, item) + isDupl[key] = false + } + } + + return result +} + // Min search the minimum value of a collection. func Min[T constraints.Ordered](collection []T) T { var min T @@ -187,19 +320,18 @@ func Last[T any](collection []T) (T, error) { // Nth returns the element at index `nth` of collection. If `nth` is negative, the nth element // from the end is returned. An error is returned when nth is out of slice bounds. -func Nth[T any](collection []T, nth int) (T, error) { - if int(math.Abs(float64(nth))) >= len(collection) { +func Nth[T any, N constraints.Integer](collection []T, nth N) (T, error) { + n := int(nth) + l := len(collection) + if n >= l || -n > l { var t T - return t, fmt.Errorf("nth: %d out of slice bounds", nth) + return t, fmt.Errorf("nth: %d out of slice bounds", n) } - length := len(collection) - - if nth >= 0 { - return collection[nth], nil + if n >= 0 { + return collection[n], nil } - - return collection[length+nth], nil + return collection[l+n], nil } // Sample returns a random item from collection. @@ -216,11 +348,7 @@ func Sample[T any](collection []T) T { func Samples[T any](collection []T, count int) []T { size := len(collection) - // put values into a map, for faster deletion - cOpy := make([]T, 0, size) - for _, v := range collection { - cOpy = append(cOpy, v) - } + cOpy := append([]T{}, collection...) results := []T{} diff --git a/vendor/github.com/samber/lo/func.go b/vendor/github.com/samber/lo/func.go new file mode 100644 index 00000000..90afc9ac --- /dev/null +++ b/vendor/github.com/samber/lo/func.go @@ -0,0 +1,8 @@ +package lo + +// Partial returns new function that, when called, has its first argument set to the provided value. +func Partial[T1, T2, R any](f func(T1, T2) R, arg1 T1) func(T2) R { + return func(t2 T2) R { + return f(arg1, t2) + } +} diff --git a/vendor/github.com/samber/lo/intersect.go b/vendor/github.com/samber/lo/intersect.go index eac16392..169cc2cf 100644 --- a/vendor/github.com/samber/lo/intersect.go +++ b/vendor/github.com/samber/lo/intersect.go @@ -175,3 +175,28 @@ func Union[T comparable](list1 []T, list2 []T) []T { return result } + +// Without returns slice excluding all given values. +func Without[T comparable](collection []T, exclude ...T) []T { + result := make([]T, 0, len(collection)) + for _, e := range collection { + if !Contains(exclude, e) { + result = append(result, e) + } + } + return result +} + +// WithoutEmpty returns slice excluding empty values. +func WithoutEmpty[T comparable](collection []T) []T { + var empty T + + result := make([]T, 0, len(collection)) + for _, e := range collection { + if e != empty { + result = append(result, e) + } + } + + return result +} diff --git a/vendor/github.com/samber/lo/map.go b/vendor/github.com/samber/lo/map.go index 294421c5..7915e37c 100644 --- a/vendor/github.com/samber/lo/map.go +++ b/vendor/github.com/samber/lo/map.go @@ -1,6 +1,7 @@ package lo // Keys creates an array of the map keys. +// Play: https://go.dev/play/p/Uu11fHASqrU func Keys[K comparable, V any](in map[K]V) []K { result := make([]K, 0, len(in)) @@ -12,6 +13,7 @@ func Keys[K comparable, V any](in map[K]V) []K { } // Values creates an array of the map values. +// Play: https://go.dev/play/p/nnRTQkzQfF6 func Values[K comparable, V any](in map[K]V) []V { result := make([]V, 0, len(in)) @@ -23,6 +25,7 @@ func Values[K comparable, V any](in map[K]V) []V { } // PickBy returns same map type filtered by given predicate. +// Play: https://go.dev/play/p/kdg8GR_QMmf func PickBy[K comparable, V any](in map[K]V, predicate func(K, V) bool) map[K]V { r := map[K]V{} for k, v := range in { @@ -34,6 +37,7 @@ func PickBy[K comparable, V any](in map[K]V, predicate func(K, V) bool) map[K]V } // PickByKeys returns same map type filtered by given keys. +// Play: https://go.dev/play/p/R1imbuci9qU func PickByKeys[K comparable, V any](in map[K]V, keys []K) map[K]V { r := map[K]V{} for k, v := range in { @@ -45,6 +49,7 @@ func PickByKeys[K comparable, V any](in map[K]V, keys []K) map[K]V { } // PickByValues returns same map type filtered by given values. +// Play: https://go.dev/play/p/1zdzSvbfsJc func PickByValues[K comparable, V comparable](in map[K]V, values []V) map[K]V { r := map[K]V{} for k, v := range in { @@ -55,7 +60,8 @@ func PickByValues[K comparable, V comparable](in map[K]V, values []V) map[K]V { return r } -// PickBy returns same map type filtered by given predicate. +// OmitBy returns same map type filtered by given predicate. +// Play: https://go.dev/play/p/EtBsR43bdsd func OmitBy[K comparable, V any](in map[K]V, predicate func(K, V) bool) map[K]V { r := map[K]V{} for k, v := range in { @@ -67,6 +73,7 @@ func OmitBy[K comparable, V any](in map[K]V, predicate func(K, V) bool) map[K]V } // OmitByKeys returns same map type filtered by given keys. +// Play: https://go.dev/play/p/t1QjCrs-ysk func OmitByKeys[K comparable, V any](in map[K]V, keys []K) map[K]V { r := map[K]V{} for k, v := range in { @@ -78,6 +85,7 @@ func OmitByKeys[K comparable, V any](in map[K]V, keys []K) map[K]V { } // OmitByValues returns same map type filtered by given values. +// Play: https://go.dev/play/p/9UYZi-hrs8j func OmitByValues[K comparable, V comparable](in map[K]V, values []V) map[K]V { r := map[K]V{} for k, v := range in { @@ -89,6 +97,7 @@ func OmitByValues[K comparable, V comparable](in map[K]V, values []V) map[K]V { } // Entries transforms a map into array of key/value pairs. +// Play: func Entries[K comparable, V any](in map[K]V) []Entry[K, V] { entries := make([]Entry[K, V], 0, len(in)) @@ -102,7 +111,15 @@ func Entries[K comparable, V any](in map[K]V) []Entry[K, V] { return entries } +// ToPairs transforms a map into array of key/value pairs. +// Alias of Entries(). +// Play: https://go.dev/play/p/3Dhgx46gawJ +func ToPairs[K comparable, V any](in map[K]V) []Entry[K, V] { + return Entries(in) +} + // FromEntries transforms an array of key/value pairs into a map. +// Play: https://go.dev/play/p/oIr5KHFGCEN func FromEntries[K comparable, V any](entries []Entry[K, V]) map[K]V { out := map[K]V{} @@ -113,9 +130,17 @@ func FromEntries[K comparable, V any](entries []Entry[K, V]) map[K]V { return out } +// FromPairs transforms an array of key/value pairs into a map. +// Alias of FromEntries(). +// Play: https://go.dev/play/p/oIr5KHFGCEN +func FromPairs[K comparable, V any](entries []Entry[K, V]) map[K]V { + return FromEntries(entries) +} + // Invert creates a map composed of the inverted keys and values. If map // contains duplicate values, subsequent values overwrite property assignments // of previous values. +// Play: https://go.dev/play/p/rFQ4rak6iA1 func Invert[K comparable, V comparable](in map[K]V) map[V]K { out := map[V]K{} @@ -127,6 +152,7 @@ func Invert[K comparable, V comparable](in map[K]V) map[V]K { } // Assign merges multiple maps from left to right. +// Play: https://go.dev/play/p/VhwfJOyxf5o func Assign[K comparable, V any](maps ...map[K]V) map[K]V { out := map[K]V{} @@ -140,6 +166,7 @@ func Assign[K comparable, V any](maps ...map[K]V) map[K]V { } // MapKeys manipulates a map keys and transforms it to a map of another type. +// Play: https://go.dev/play/p/9_4WPIqOetJ func MapKeys[K comparable, V any, R comparable](in map[K]V, iteratee func(V, K) R) map[R]V { result := map[R]V{} @@ -151,6 +178,7 @@ func MapKeys[K comparable, V any, R comparable](in map[K]V, iteratee func(V, K) } // MapValues manipulates a map values and transforms it to a map of another type. +// Play: https://go.dev/play/p/T_8xAfvcf0W func MapValues[K comparable, V any, R any](in map[K]V, iteratee func(V, K) R) map[K]R { result := map[K]R{} @@ -160,3 +188,15 @@ func MapValues[K comparable, V any, R any](in map[K]V, iteratee func(V, K) R) ma return result } + +// MapToSlice transforms a map into a slice based on specific iteratee +// Play: https://go.dev/play/p/ZuiCZpDt6LD +func MapToSlice[K comparable, V any, R any](in map[K]V, iteratee func(K, V) R) []R { + result := make([]R, 0, len(in)) + + for k, v := range in { + result = append(result, iteratee(k, v)) + } + + return result +} diff --git a/vendor/github.com/samber/lo/math.go b/vendor/github.com/samber/lo/math.go index 1ce1ebe8..30e7e9ef 100644 --- a/vendor/github.com/samber/lo/math.go +++ b/vendor/github.com/samber/lo/math.go @@ -3,6 +3,7 @@ package lo import "golang.org/x/exp/constraints" // Range creates an array of numbers (positive and/or negative) with given length. +// Play: https://go.dev/play/p/0r6VimXAi9H func Range(elementNum int) []int { length := If(elementNum < 0, -elementNum).Else(elementNum) result := make([]int, length) @@ -14,6 +15,7 @@ func Range(elementNum int) []int { } // RangeFrom creates an array of numbers from start with specified length. +// Play: https://go.dev/play/p/0r6VimXAi9H func RangeFrom[T constraints.Integer | constraints.Float](start T, elementNum int) []T { length := If(elementNum < 0, -elementNum).Else(elementNum) result := make([]T, length) @@ -26,6 +28,7 @@ func RangeFrom[T constraints.Integer | constraints.Float](start T, elementNum in // RangeWithSteps creates an array of numbers (positive and/or negative) progressing from start up to, but not including end. // step set to zero will return empty array. +// Play: https://go.dev/play/p/0r6VimXAi9H func RangeWithSteps[T constraints.Integer | constraints.Float](start, end, step T) []T { result := []T{} if start == end || step == 0 { @@ -50,6 +53,7 @@ func RangeWithSteps[T constraints.Integer | constraints.Float](start, end, step } // Clamp clamps number within the inclusive lower and upper bounds. +// Play: https://go.dev/play/p/RU4lJNC2hlI func Clamp[T constraints.Ordered](value T, min T, max T) T { if value < min { return min @@ -59,7 +63,8 @@ func Clamp[T constraints.Ordered](value T, min T, max T) T { return value } -// Summarizes the values in a collection. +// SumBy summarizes the values in a collection using the given return value from the iteration function. If collection is empty 0 is returned. +// Play: https://go.dev/play/p/Dz_a_7jN_ca func SumBy[T any, R constraints.Float | constraints.Integer](collection []T, iteratee func(T) R) R { var sum R = 0 for _, item := range collection { diff --git a/vendor/github.com/samber/lo/pointers.go b/vendor/github.com/samber/lo/pointers.go deleted file mode 100644 index 623d547d..00000000 --- a/vendor/github.com/samber/lo/pointers.go +++ /dev/null @@ -1,32 +0,0 @@ -package lo - -// ToPtr returns a pointer copy of value. -func ToPtr[T any](x T) *T { - return &x -} - -// ToSlicePtr returns a slice of pointer copy of value. -func ToSlicePtr[T any](collection []T) []*T { - return Map(collection, func(x T, _ int) *T { - return &x - }) -} - -// Empty returns an empty value. -func Empty[T any]() T { - var t T - return t -} - -// Coalesce returns the first non-empty arguments. Arguments must be comparable. -func Coalesce[T comparable](v ...T) (result T, ok bool) { - for _, e := range v { - if e != result { - result = e - ok = true - return - } - } - - return -} diff --git a/vendor/github.com/samber/lo/retry.go b/vendor/github.com/samber/lo/retry.go index 7d86795d..b4a61efb 100644 --- a/vendor/github.com/samber/lo/retry.go +++ b/vendor/github.com/samber/lo/retry.go @@ -46,6 +46,7 @@ func (d *debounce) cancel() { } // NewDebounce creates a debounced instance that delays invoking functions given until after wait milliseconds have elapsed. +// Play: https://go.dev/play/p/mz32VMK2nqe func NewDebounce(duration time.Duration, f ...func()) (func(), func()) { d := &debounce{ after: duration, @@ -60,7 +61,8 @@ func NewDebounce(duration time.Duration, f ...func()) (func(), func()) { }, d.cancel } -// Attempt invokes a function N times until it returns valid output. Returning either the caught error or nil. When first argument is less than `1`, the function runs until a sucessfull response is returned. +// Attempt invokes a function N times until it returns valid output. Returning either the caught error or nil. When first argument is less than `1`, the function runs until a successful response is returned. +// Play: https://go.dev/play/p/3ggJZ2ZKcMj func Attempt(maxIteration int, f func(int) error) (int, error) { var err error @@ -76,9 +78,10 @@ func Attempt(maxIteration int, f func(int) error) (int, error) { } // AttemptWithDelay invokes a function N times until it returns valid output, -// with a pause betwwen each call. Returning either the caught error or nil. -// When first argument is less than `1`, the function runs until a sucessfull +// with a pause between each call. Returning either the caught error or nil. +// When first argument is less than `1`, the function runs until a successful // response is returned. +// Play: https://go.dev/play/p/tVs6CygC7m1 func AttemptWithDelay(maxIteration int, delay time.Duration, f func(int, time.Duration) error) (int, time.Duration, error) { var err error diff --git a/vendor/github.com/samber/lo/slice.go b/vendor/github.com/samber/lo/slice.go index 04d048be..516fc8b1 100644 --- a/vendor/github.com/samber/lo/slice.go +++ b/vendor/github.com/samber/lo/slice.go @@ -2,9 +2,12 @@ package lo import ( "math/rand" + + "golang.org/x/exp/constraints" ) // Filter iterates over elements of collection, returning an array of all elements predicate returns truthy for. +// Play: https://go.dev/play/p/Apjg3WeSi7K func Filter[V any](collection []V, predicate func(V, int) bool) []V { result := []V{} @@ -18,6 +21,7 @@ func Filter[V any](collection []V, predicate func(V, int) bool) []V { } // Map manipulates a slice and transforms it to a slice of another type. +// Play: https://go.dev/play/p/OkPcYAhBo0D func Map[T any, R any](collection []T, iteratee func(T, int) R) []R { result := make([]R, len(collection)) @@ -32,6 +36,8 @@ func Map[T any, R any](collection []T, iteratee func(T, int) R) []R { // The callback function should return two values: // - the result of the mapping operation and // - whether the result element should be included or not. +// +// Play: https://go.dev/play/p/-AuYXfy7opz func FilterMap[T any, R any](collection []T, callback func(T, int) (R, bool)) []R { result := []R{} @@ -45,6 +51,7 @@ func FilterMap[T any, R any](collection []T, callback func(T, int) (R, bool)) [] } // FlatMap manipulates a slice and transforms and flattens it to a slice of another type. +// Play: https://go.dev/play/p/YSoYmQTA8-U func FlatMap[T any, R any](collection []T, iteratee func(T, int) []R) []R { result := []R{} @@ -57,6 +64,7 @@ func FlatMap[T any, R any](collection []T, iteratee func(T, int) []R) []R { // Reduce reduces collection to a value which is the accumulated result of running each element in collection // through accumulator, where each successive invocation is supplied the return value of the previous. +// Play: https://go.dev/play/p/R4UHXZNaaUG func Reduce[T any, R any](collection []T, accumulator func(R, T, int) R, initial R) R { for i, item := range collection { initial = accumulator(initial, item, i) @@ -65,7 +73,18 @@ func Reduce[T any, R any](collection []T, accumulator func(R, T, int) R, initial return initial } +// ReduceRight helper is like Reduce except that it iterates over elements of collection from right to left. +// Play: https://go.dev/play/p/Fq3W70l7wXF +func ReduceRight[T any, R any](collection []T, accumulator func(R, T, int) R, initial R) R { + for i := len(collection) - 1; i >= 0; i-- { + initial = accumulator(initial, collection[i], i) + } + + return initial +} + // ForEach iterates over elements of collection and invokes iteratee for each element. +// Play: https://go.dev/play/p/oofyiUPRf8t func ForEach[T any](collection []T, iteratee func(T, int)) { for i, item := range collection { iteratee(item, i) @@ -74,6 +93,7 @@ func ForEach[T any](collection []T, iteratee func(T, int)) { // Times invokes the iteratee n times, returning an array of the results of each invocation. // The iteratee is invoked with index as argument. +// Play: https://go.dev/play/p/vgQj3Glr6lT func Times[T any](count int, iteratee func(int) T) []T { result := make([]T, count) @@ -86,6 +106,7 @@ func Times[T any](count int, iteratee func(int) T) []T { // Uniq returns a duplicate-free version of an array, in which only the first occurrence of each element is kept. // The order of result values is determined by the order they occur in the array. +// Play: https://go.dev/play/p/DTzbeXZ6iEN func Uniq[T comparable](collection []T) []T { result := make([]T, 0, len(collection)) seen := make(map[T]struct{}, len(collection)) @@ -105,6 +126,7 @@ func Uniq[T comparable](collection []T) []T { // UniqBy returns a duplicate-free version of an array, in which only the first occurrence of each element is kept. // The order of result values is determined by the order they occur in the array. It accepts `iteratee` which is // invoked for each element in array to generate the criterion by which uniqueness is computed. +// Play: https://go.dev/play/p/g42Z3QSb53u func UniqBy[T any, U comparable](collection []T, iteratee func(T) U) []T { result := make([]T, 0, len(collection)) seen := make(map[U]struct{}, len(collection)) @@ -124,6 +146,7 @@ func UniqBy[T any, U comparable](collection []T, iteratee func(T) U) []T { } // GroupBy returns an object composed of keys generated from the results of running each element of collection through iteratee. +// Play: https://go.dev/play/p/XnQBd_v6brd func GroupBy[T any, U comparable](collection []T, iteratee func(T) U) map[U][]T { result := map[U][]T{} @@ -138,22 +161,25 @@ func GroupBy[T any, U comparable](collection []T, iteratee func(T) U) map[U][]T // Chunk returns an array of elements split into groups the length of size. If array can't be split evenly, // the final chunk will be the remaining elements. +// Play: https://go.dev/play/p/EeKl0AuTehH func Chunk[T any](collection []T, size int) [][]T { if size <= 0 { panic("Second parameter must be greater than 0") } - result := make([][]T, 0, len(collection)/2+1) - length := len(collection) + chunksNum := len(collection) / size + if len(collection)%size != 0 { + chunksNum += 1 + } - for i := 0; i < length; i++ { - chunk := i / size + result := make([][]T, 0, chunksNum) - if i%size == 0 { - result = append(result, make([]T, 0, size)) + for i := 0; i < chunksNum; i++ { + last := (i + 1) * size + if last > len(collection) { + last = len(collection) } - - result[chunk] = append(result[chunk], collection[i]) + result = append(result, collection[i*size:last]) } return result @@ -162,6 +188,7 @@ func Chunk[T any](collection []T, size int) [][]T { // PartitionBy returns an array of elements split into groups. The order of grouped values is // determined by the order they occur in collection. The grouping is generated from the results // of running each element of collection through iteratee. +// Play: https://go.dev/play/p/NfQ_nGjkgXW func PartitionBy[T any, K comparable](collection []T, iteratee func(x T) K) [][]T { result := [][]T{} seen := map[K]int{} @@ -187,17 +214,23 @@ func PartitionBy[T any, K comparable](collection []T, iteratee func(x T) K) [][] } // Flatten returns an array a single level deep. +// Play: https://go.dev/play/p/rbp9ORaMpjw func Flatten[T any](collection [][]T) []T { - result := []T{} + totalLen := 0 + for i := range collection { + totalLen += len(collection[i]) + } - for _, item := range collection { - result = append(result, item...) + result := make([]T, 0, totalLen) + for i := range collection { + result = append(result, collection[i]...) } return result } // Shuffle returns an array of shuffled values. Uses the Fisher-Yates shuffle algorithm. +// Play: https://go.dev/play/p/Qp73bnTDnc7 func Shuffle[T any](collection []T) []T { rand.Shuffle(len(collection), func(i, j int) { collection[i], collection[j] = collection[j], collection[i] @@ -207,6 +240,7 @@ func Shuffle[T any](collection []T) []T { } // Reverse reverses array so that the first element becomes the last, the second element becomes the second to last, and so on. +// Play: https://go.dev/play/p/fhUMLvZ7vS6 func Reverse[T any](collection []T) []T { length := len(collection) half := length / 2 @@ -220,6 +254,7 @@ func Reverse[T any](collection []T) []T { } // Fill fills elements of array with `initial` value. +// Play: https://go.dev/play/p/VwR34GzqEub func Fill[T Clonable[T]](collection []T, initial T) []T { result := make([]T, 0, len(collection)) @@ -231,6 +266,7 @@ func Fill[T Clonable[T]](collection []T, initial T) []T { } // Repeat builds a slice with N copies of initial value. +// Play: https://go.dev/play/p/g3uHXbmc3b6 func Repeat[T Clonable[T]](count int, initial T) []T { result := make([]T, 0, count) @@ -242,6 +278,7 @@ func Repeat[T Clonable[T]](count int, initial T) []T { } // RepeatBy builds a slice with values returned by N calls of callback. +// Play: https://go.dev/play/p/ozZLCtX_hNU func RepeatBy[T any](count int, predicate func(int) T) []T { result := make([]T, 0, count) @@ -253,6 +290,7 @@ func RepeatBy[T any](count int, predicate func(int) T) []T { } // KeyBy transforms a slice or an array of structs to a map based on a pivot callback. +// Play: https://go.dev/play/p/mdaClUAT-zZ func KeyBy[K comparable, V any](collection []V, iteratee func(V) K) map[K]V { result := make(map[K]V, len(collection)) @@ -264,21 +302,55 @@ func KeyBy[K comparable, V any](collection []V, iteratee func(V) K) map[K]V { return result } -// Drop drops n elements from the beginning of a slice or array. -func Drop[T any](collection []T, n int) []T { - if len(collection) <= n { - return make([]T, 0) - } +// Associate returns a map containing key-value pairs provided by transform function applied to elements of the given slice. +// If any of two pairs would have the same key the last one gets added to the map. +// The order of keys in returned map is not specified and is not guaranteed to be the same from the original array. +// Play: https://go.dev/play/p/WHa2CfMO3Lr +func Associate[T any, K comparable, V any](collection []T, transform func(T) (K, V)) map[K]V { + result := make(map[K]V) - result := make([]T, len(collection)-n) - for i := n; i < len(collection); i++ { - result[i-n] = collection[i] + for _, t := range collection { + k, v := transform(t) + result[k] = v } return result } +// SliceToMap returns a map containing key-value pairs provided by transform function applied to elements of the given slice. +// If any of two pairs would have the same key the last one gets added to the map. +// The order of keys in returned map is not specified and is not guaranteed to be the same from the original array. +// Alias of Associate(). +// Play: https://go.dev/play/p/WHa2CfMO3Lr +func SliceToMap[T any, K comparable, V any](collection []T, transform func(T) (K, V)) map[K]V { + return Associate(collection, transform) +} + +// Drop drops n elements from the beginning of a slice or array. +// Play: https://go.dev/play/p/JswS7vXRJP2 +func Drop[T any](collection []T, n int) []T { + if len(collection) <= n { + return make([]T, 0) + } + + result := make([]T, 0, len(collection)-n) + + return append(result, collection[n:]...) +} + +// DropRight drops n elements from the end of a slice or array. +// Play: https://go.dev/play/p/GG0nXkSJJa3 +func DropRight[T any](collection []T, n int) []T { + if len(collection) <= n { + return []T{} + } + + result := make([]T, 0, len(collection)-n) + return append(result, collection[:len(collection)-n]...) +} + // DropWhile drops elements from the beginning of a slice or array while the predicate returns true. +// Play: https://go.dev/play/p/7gBPYw2IK16 func DropWhile[T any](collection []T, predicate func(T) bool) []T { i := 0 for ; i < len(collection); i++ { @@ -287,30 +359,12 @@ func DropWhile[T any](collection []T, predicate func(T) bool) []T { } } - result := make([]T, len(collection)-i) - - for j := 0; i < len(collection); i, j = i+1, j+1 { - result[j] = collection[i] - } - - return result -} - -// DropRight drops n elements from the end of a slice or array. -func DropRight[T any](collection []T, n int) []T { - if len(collection) <= n { - return make([]T, 0) - } - - result := make([]T, len(collection)-n) - for i := len(collection) - 1 - n; i >= 0; i-- { - result[i] = collection[i] - } - - return result + result := make([]T, 0, len(collection)-i) + return append(result, collection[i:]...) } // DropRightWhile drops elements from the end of a slice or array while the predicate returns true. +// Play: https://go.dev/play/p/3-n71oEC0Hz func DropRightWhile[T any](collection []T, predicate func(T) bool) []T { i := len(collection) - 1 for ; i >= 0; i-- { @@ -319,16 +373,12 @@ func DropRightWhile[T any](collection []T, predicate func(T) bool) []T { } } - result := make([]T, i+1) - - for ; i >= 0; i-- { - result[i] = collection[i] - } - - return result + result := make([]T, 0, i+1) + return append(result, collection[:i+1]...) } // Reject is the opposite of Filter, this method returns the elements of collection that predicate does not return truthy for. +// Play: https://go.dev/play/p/YkLMODy1WEL func Reject[V any](collection []V, predicate func(V, int) bool) []V { result := []V{} @@ -342,6 +392,7 @@ func Reject[V any](collection []V, predicate func(V, int) bool) []V { } // Count counts the number of elements in the collection that compare equal to value. +// Play: https://go.dev/play/p/Y3FlK54yveC func Count[T comparable](collection []T, value T) (count int) { for _, item := range collection { if item == value { @@ -353,6 +404,7 @@ func Count[T comparable](collection []T, value T) (count int) { } // CountBy counts the number of elements in the collection for which predicate is true. +// Play: https://go.dev/play/p/ByQbNYQQi4X func CountBy[T any](collection []T, predicate func(T) bool) (count int) { for _, item := range collection { if predicate(item) { @@ -363,7 +415,8 @@ func CountBy[T any](collection []T, predicate func(T) bool) (count int) { return count } -// Subset return part of a slice. +// Subset returns a copy of a slice from `offset` up to `length` elements. Like `slice[start:start+length]`, but does not panic on overflow. +// Play: https://go.dev/play/p/tOQu1GhFcog func Subset[T any](collection []T, offset int, length uint) []T { size := len(collection) @@ -385,17 +438,36 @@ func Subset[T any](collection []T, offset int, length uint) []T { return collection[offset : offset+int(length)] } -// Replace returns a copy of the slice with the first n non-overlapping instances of old replaced by new. -func Replace[T comparable](collection []T, old T, new T, n int) []T { +// Slice returns a copy of a slice from `start` up to, but not including `end`. Like `slice[start:end]`, but does not panic on overflow. +// Play: https://go.dev/play/p/8XWYhfMMA1h +func Slice[T any](collection []T, start int, end int) []T { size := len(collection) - result := make([]T, 0, size) - for _, item := range collection { - if item == old && n != 0 { - result = append(result, new) + if start >= end { + return []T{} + } + + if start > size { + start = size + } + + if end > size { + end = size + } + + return collection[start:end] +} + +// Replace returns a copy of the slice with the first n non-overlapping instances of old replaced by new. +// Play: https://go.dev/play/p/XfPzmf9gql6 +func Replace[T comparable](collection []T, old T, new T, n int) []T { + result := make([]T, len(collection)) + copy(result, collection) + + for i := range result { + if result[i] == old && n != 0 { + result[i] = new n-- - } else { - result = append(result, item) } } @@ -403,6 +475,49 @@ func Replace[T comparable](collection []T, old T, new T, n int) []T { } // ReplaceAll returns a copy of the slice with all non-overlapping instances of old replaced by new. +// Play: https://go.dev/play/p/a9xZFUHfYcV func ReplaceAll[T comparable](collection []T, old T, new T) []T { - return Replace[T](collection, old, new, -1) + return Replace(collection, old, new, -1) +} + +// Compact returns a slice of all non-zero elements. +// Play: https://go.dev/play/p/tXiy-iK6PAc +func Compact[T comparable](collection []T) []T { + var zero T + + result := []T{} + + for _, item := range collection { + if item != zero { + result = append(result, item) + } + } + + return result +} + +// IsSorted checks if a slice is sorted. +// Play: https://go.dev/play/p/mc3qR-t4mcx +func IsSorted[T constraints.Ordered](collection []T) bool { + for i := 1; i < len(collection); i++ { + if collection[i-1] > collection[i] { + return false + } + } + + return true +} + +// IsSortedByKey checks if a slice is sorted by iteratee. +// Play: https://go.dev/play/p/wiG6XyBBu49 +func IsSortedByKey[T any, K constraints.Ordered](collection []T, iteratee func(T) K) bool { + size := len(collection) + + for i := 0; i < size-1; i++ { + if iteratee(collection[i]) > iteratee(collection[i+1]) { + return false + } + } + + return true } diff --git a/vendor/github.com/samber/lo/string.go b/vendor/github.com/samber/lo/string.go index f60f764a..a63167cb 100644 --- a/vendor/github.com/samber/lo/string.go +++ b/vendor/github.com/samber/lo/string.go @@ -1,8 +1,11 @@ package lo -import "unicode/utf8" +import ( + "unicode/utf8" +) // Substring return part of a string. +// Play: https://go.dev/play/p/TQlxQi82Lu1 func Substring[T ~string](str T, offset int, length uint) T { size := len(str) @@ -24,7 +27,39 @@ func Substring[T ~string](str T, offset int, length uint) T { return str[offset : offset+int(length)] } +// ChunkString returns an array of strings split into groups the length of size. If array can't be split evenly, +// the final chunk will be the remaining elements. +// Play: https://go.dev/play/p/__FLTuJVz54 +func ChunkString[T ~string](str T, size int) []T { + if size <= 0 { + panic("lo.ChunkString: Size parameter must be greater than 0") + } + + if len(str) == 0 { + return []T{""} + } + + if size >= len(str) { + return []T{str} + } + + var chunks []T = make([]T, 0, ((len(str)-1)/size)+1) + currentLen := 0 + currentStart := 0 + for i := range str { + if currentLen == size { + chunks = append(chunks, str[currentStart:i]) + currentLen = 0 + currentStart = i + } + currentLen++ + } + chunks = append(chunks, str[currentStart:]) + return chunks +} + // RuneLength is an alias to utf8.RuneCountInString which returns the number of runes in string. +// Play: https://go.dev/play/p/tuhgW_lWY8l func RuneLength(str string) int { return utf8.RuneCountInString(str) } diff --git a/vendor/github.com/samber/lo/test.go b/vendor/github.com/samber/lo/test.go new file mode 100644 index 00000000..26db4c25 --- /dev/null +++ b/vendor/github.com/samber/lo/test.go @@ -0,0 +1,32 @@ +package lo + +import ( + "os" + "testing" + "time" +) + +// https://github.com/stretchr/testify/issues/1101 +func testWithTimeout(t *testing.T, timeout time.Duration) { + t.Helper() + + testFinished := make(chan struct{}) + t.Cleanup(func() { close(testFinished) }) + + go func() { + select { + case <-testFinished: + case <-time.After(timeout): + t.Errorf("test timed out after %s", timeout) + os.Exit(1) + } + }() +} + +type foo struct { + bar string +} + +func (f foo) Clone() foo { + return foo{f.bar} +} diff --git a/vendor/github.com/samber/lo/tuples.go b/vendor/github.com/samber/lo/tuples.go index d5f7a7bc..f8ea2f14 100644 --- a/vendor/github.com/samber/lo/tuples.go +++ b/vendor/github.com/samber/lo/tuples.go @@ -1,81 +1,97 @@ package lo // T2 creates a tuple from a list of values. +// Play: https://go.dev/play/p/IllL3ZO4BQm func T2[A any, B any](a A, b B) Tuple2[A, B] { return Tuple2[A, B]{A: a, B: b} } // T3 creates a tuple from a list of values. +// Play: https://go.dev/play/p/IllL3ZO4BQm func T3[A any, B any, C any](a A, b B, c C) Tuple3[A, B, C] { return Tuple3[A, B, C]{A: a, B: b, C: c} } // T4 creates a tuple from a list of values. +// Play: https://go.dev/play/p/IllL3ZO4BQm func T4[A any, B any, C any, D any](a A, b B, c C, d D) Tuple4[A, B, C, D] { return Tuple4[A, B, C, D]{A: a, B: b, C: c, D: d} } // T5 creates a tuple from a list of values. +// Play: https://go.dev/play/p/IllL3ZO4BQm func T5[A any, B any, C any, D any, E any](a A, b B, c C, d D, e E) Tuple5[A, B, C, D, E] { return Tuple5[A, B, C, D, E]{A: a, B: b, C: c, D: d, E: e} } // T6 creates a tuple from a list of values. +// Play: https://go.dev/play/p/IllL3ZO4BQm func T6[A any, B any, C any, D any, E any, F any](a A, b B, c C, d D, e E, f F) Tuple6[A, B, C, D, E, F] { return Tuple6[A, B, C, D, E, F]{A: a, B: b, C: c, D: d, E: e, F: f} } // T7 creates a tuple from a list of values. +// Play: https://go.dev/play/p/IllL3ZO4BQm func T7[A any, B any, C any, D any, E any, F any, G any](a A, b B, c C, d D, e E, f F, g G) Tuple7[A, B, C, D, E, F, G] { return Tuple7[A, B, C, D, E, F, G]{A: a, B: b, C: c, D: d, E: e, F: f, G: g} } // T8 creates a tuple from a list of values. +// Play: https://go.dev/play/p/IllL3ZO4BQm func T8[A any, B any, C any, D any, E any, F any, G any, H any](a A, b B, c C, d D, e E, f F, g G, h H) Tuple8[A, B, C, D, E, F, G, H] { return Tuple8[A, B, C, D, E, F, G, H]{A: a, B: b, C: c, D: d, E: e, F: f, G: g, H: h} } // T8 creates a tuple from a list of values. +// Play: https://go.dev/play/p/IllL3ZO4BQm func T9[A any, B any, C any, D any, E any, F any, G any, H any, I any](a A, b B, c C, d D, e E, f F, g G, h H, i I) Tuple9[A, B, C, D, E, F, G, H, I] { return Tuple9[A, B, C, D, E, F, G, H, I]{A: a, B: b, C: c, D: d, E: e, F: f, G: g, H: h, I: i} } // Unpack2 returns values contained in tuple. +// Play: https://go.dev/play/p/xVP_k0kJ96W func Unpack2[A any, B any](tuple Tuple2[A, B]) (A, B) { return tuple.A, tuple.B } // Unpack3 returns values contained in tuple. +// Play: https://go.dev/play/p/xVP_k0kJ96W func Unpack3[A any, B any, C any](tuple Tuple3[A, B, C]) (A, B, C) { return tuple.A, tuple.B, tuple.C } // Unpack4 returns values contained in tuple. +// Play: https://go.dev/play/p/xVP_k0kJ96W func Unpack4[A any, B any, C any, D any](tuple Tuple4[A, B, C, D]) (A, B, C, D) { return tuple.A, tuple.B, tuple.C, tuple.D } // Unpack5 returns values contained in tuple. +// Play: https://go.dev/play/p/xVP_k0kJ96W func Unpack5[A any, B any, C any, D any, E any](tuple Tuple5[A, B, C, D, E]) (A, B, C, D, E) { return tuple.A, tuple.B, tuple.C, tuple.D, tuple.E } // Unpack6 returns values contained in tuple. +// Play: https://go.dev/play/p/xVP_k0kJ96W func Unpack6[A any, B any, C any, D any, E any, F any](tuple Tuple6[A, B, C, D, E, F]) (A, B, C, D, E, F) { return tuple.A, tuple.B, tuple.C, tuple.D, tuple.E, tuple.F } // Unpack7 returns values contained in tuple. +// Play: https://go.dev/play/p/xVP_k0kJ96W func Unpack7[A any, B any, C any, D any, E any, F any, G any](tuple Tuple7[A, B, C, D, E, F, G]) (A, B, C, D, E, F, G) { return tuple.A, tuple.B, tuple.C, tuple.D, tuple.E, tuple.F, tuple.G } // Unpack8 returns values contained in tuple. +// Play: https://go.dev/play/p/xVP_k0kJ96W func Unpack8[A any, B any, C any, D any, E any, F any, G any, H any](tuple Tuple8[A, B, C, D, E, F, G, H]) (A, B, C, D, E, F, G, H) { return tuple.A, tuple.B, tuple.C, tuple.D, tuple.E, tuple.F, tuple.G, tuple.H } // Unpack9 returns values contained in tuple. +// Play: https://go.dev/play/p/xVP_k0kJ96W func Unpack9[A any, B any, C any, D any, E any, F any, G any, H any, I any](tuple Tuple9[A, B, C, D, E, F, G, H, I]) (A, B, C, D, E, F, G, H, I) { return tuple.A, tuple.B, tuple.C, tuple.D, tuple.E, tuple.F, tuple.G, tuple.H, tuple.I } @@ -83,14 +99,15 @@ func Unpack9[A any, B any, C any, D any, E any, F any, G any, H any, I any](tupl // Zip2 creates a slice of grouped elements, the first of which contains the first elements // of the given arrays, the second of which contains the second elements of the given arrays, and so on. // When collections have different size, the Tuple attributes are filled with zero value. +// Play: https://go.dev/play/p/jujaA6GaJTp func Zip2[A any, B any](a []A, b []B) []Tuple2[A, B] { - size := Max[int]([]int{len(a), len(b)}) + size := Max([]int{len(a), len(b)}) result := make([]Tuple2[A, B], 0, size) for index := 0; index < size; index++ { - _a, _ := Nth[A](a, index) - _b, _ := Nth[B](b, index) + _a, _ := Nth(a, index) + _b, _ := Nth(b, index) result = append(result, Tuple2[A, B]{ A: _a, @@ -104,15 +121,16 @@ func Zip2[A any, B any](a []A, b []B) []Tuple2[A, B] { // Zip3 creates a slice of grouped elements, the first of which contains the first elements // of the given arrays, the second of which contains the second elements of the given arrays, and so on. // When collections have different size, the Tuple attributes are filled with zero value. +// Play: https://go.dev/play/p/jujaA6GaJTp func Zip3[A any, B any, C any](a []A, b []B, c []C) []Tuple3[A, B, C] { - size := Max[int]([]int{len(a), len(b), len(c)}) + size := Max([]int{len(a), len(b), len(c)}) result := make([]Tuple3[A, B, C], 0, size) for index := 0; index < size; index++ { - _a, _ := Nth[A](a, index) - _b, _ := Nth[B](b, index) - _c, _ := Nth[C](c, index) + _a, _ := Nth(a, index) + _b, _ := Nth(b, index) + _c, _ := Nth(c, index) result = append(result, Tuple3[A, B, C]{ A: _a, @@ -127,16 +145,17 @@ func Zip3[A any, B any, C any](a []A, b []B, c []C) []Tuple3[A, B, C] { // Zip4 creates a slice of grouped elements, the first of which contains the first elements // of the given arrays, the second of which contains the second elements of the given arrays, and so on. // When collections have different size, the Tuple attributes are filled with zero value. +// Play: https://go.dev/play/p/jujaA6GaJTp func Zip4[A any, B any, C any, D any](a []A, b []B, c []C, d []D) []Tuple4[A, B, C, D] { - size := Max[int]([]int{len(a), len(b), len(c), len(d)}) + size := Max([]int{len(a), len(b), len(c), len(d)}) result := make([]Tuple4[A, B, C, D], 0, size) for index := 0; index < size; index++ { - _a, _ := Nth[A](a, index) - _b, _ := Nth[B](b, index) - _c, _ := Nth[C](c, index) - _d, _ := Nth[D](d, index) + _a, _ := Nth(a, index) + _b, _ := Nth(b, index) + _c, _ := Nth(c, index) + _d, _ := Nth(d, index) result = append(result, Tuple4[A, B, C, D]{ A: _a, @@ -152,17 +171,18 @@ func Zip4[A any, B any, C any, D any](a []A, b []B, c []C, d []D) []Tuple4[A, B, // Zip5 creates a slice of grouped elements, the first of which contains the first elements // of the given arrays, the second of which contains the second elements of the given arrays, and so on. // When collections have different size, the Tuple attributes are filled with zero value. +// Play: https://go.dev/play/p/jujaA6GaJTp func Zip5[A any, B any, C any, D any, E any](a []A, b []B, c []C, d []D, e []E) []Tuple5[A, B, C, D, E] { - size := Max[int]([]int{len(a), len(b), len(c), len(d), len(e)}) + size := Max([]int{len(a), len(b), len(c), len(d), len(e)}) result := make([]Tuple5[A, B, C, D, E], 0, size) for index := 0; index < size; index++ { - _a, _ := Nth[A](a, index) - _b, _ := Nth[B](b, index) - _c, _ := Nth[C](c, index) - _d, _ := Nth[D](d, index) - _e, _ := Nth[E](e, index) + _a, _ := Nth(a, index) + _b, _ := Nth(b, index) + _c, _ := Nth(c, index) + _d, _ := Nth(d, index) + _e, _ := Nth(e, index) result = append(result, Tuple5[A, B, C, D, E]{ A: _a, @@ -179,18 +199,19 @@ func Zip5[A any, B any, C any, D any, E any](a []A, b []B, c []C, d []D, e []E) // Zip6 creates a slice of grouped elements, the first of which contains the first elements // of the given arrays, the second of which contains the second elements of the given arrays, and so on. // When collections have different size, the Tuple attributes are filled with zero value. +// Play: https://go.dev/play/p/jujaA6GaJTp func Zip6[A any, B any, C any, D any, E any, F any](a []A, b []B, c []C, d []D, e []E, f []F) []Tuple6[A, B, C, D, E, F] { - size := Max[int]([]int{len(a), len(b), len(c), len(d), len(e), len(f)}) + size := Max([]int{len(a), len(b), len(c), len(d), len(e), len(f)}) result := make([]Tuple6[A, B, C, D, E, F], 0, size) for index := 0; index < size; index++ { - _a, _ := Nth[A](a, index) - _b, _ := Nth[B](b, index) - _c, _ := Nth[C](c, index) - _d, _ := Nth[D](d, index) - _e, _ := Nth[E](e, index) - _f, _ := Nth[F](f, index) + _a, _ := Nth(a, index) + _b, _ := Nth(b, index) + _c, _ := Nth(c, index) + _d, _ := Nth(d, index) + _e, _ := Nth(e, index) + _f, _ := Nth(f, index) result = append(result, Tuple6[A, B, C, D, E, F]{ A: _a, @@ -208,19 +229,20 @@ func Zip6[A any, B any, C any, D any, E any, F any](a []A, b []B, c []C, d []D, // Zip7 creates a slice of grouped elements, the first of which contains the first elements // of the given arrays, the second of which contains the second elements of the given arrays, and so on. // When collections have different size, the Tuple attributes are filled with zero value. +// Play: https://go.dev/play/p/jujaA6GaJTp func Zip7[A any, B any, C any, D any, E any, F any, G any](a []A, b []B, c []C, d []D, e []E, f []F, g []G) []Tuple7[A, B, C, D, E, F, G] { - size := Max[int]([]int{len(a), len(b), len(c), len(d), len(e), len(f), len(g)}) + size := Max([]int{len(a), len(b), len(c), len(d), len(e), len(f), len(g)}) result := make([]Tuple7[A, B, C, D, E, F, G], 0, size) for index := 0; index < size; index++ { - _a, _ := Nth[A](a, index) - _b, _ := Nth[B](b, index) - _c, _ := Nth[C](c, index) - _d, _ := Nth[D](d, index) - _e, _ := Nth[E](e, index) - _f, _ := Nth[F](f, index) - _g, _ := Nth[G](g, index) + _a, _ := Nth(a, index) + _b, _ := Nth(b, index) + _c, _ := Nth(c, index) + _d, _ := Nth(d, index) + _e, _ := Nth(e, index) + _f, _ := Nth(f, index) + _g, _ := Nth(g, index) result = append(result, Tuple7[A, B, C, D, E, F, G]{ A: _a, @@ -239,20 +261,21 @@ func Zip7[A any, B any, C any, D any, E any, F any, G any](a []A, b []B, c []C, // Zip8 creates a slice of grouped elements, the first of which contains the first elements // of the given arrays, the second of which contains the second elements of the given arrays, and so on. // When collections have different size, the Tuple attributes are filled with zero value. +// Play: https://go.dev/play/p/jujaA6GaJTp func Zip8[A any, B any, C any, D any, E any, F any, G any, H any](a []A, b []B, c []C, d []D, e []E, f []F, g []G, h []H) []Tuple8[A, B, C, D, E, F, G, H] { - size := Max[int]([]int{len(a), len(b), len(c), len(d), len(e), len(f), len(g), len(h)}) + size := Max([]int{len(a), len(b), len(c), len(d), len(e), len(f), len(g), len(h)}) result := make([]Tuple8[A, B, C, D, E, F, G, H], 0, size) for index := 0; index < size; index++ { - _a, _ := Nth[A](a, index) - _b, _ := Nth[B](b, index) - _c, _ := Nth[C](c, index) - _d, _ := Nth[D](d, index) - _e, _ := Nth[E](e, index) - _f, _ := Nth[F](f, index) - _g, _ := Nth[G](g, index) - _h, _ := Nth[H](h, index) + _a, _ := Nth(a, index) + _b, _ := Nth(b, index) + _c, _ := Nth(c, index) + _d, _ := Nth(d, index) + _e, _ := Nth(e, index) + _f, _ := Nth(f, index) + _g, _ := Nth(g, index) + _h, _ := Nth(h, index) result = append(result, Tuple8[A, B, C, D, E, F, G, H]{ A: _a, @@ -272,21 +295,22 @@ func Zip8[A any, B any, C any, D any, E any, F any, G any, H any](a []A, b []B, // Zip9 creates a slice of grouped elements, the first of which contains the first elements // of the given arrays, the second of which contains the second elements of the given arrays, and so on. // When collections have different size, the Tuple attributes are filled with zero value. +// Play: https://go.dev/play/p/jujaA6GaJTp func Zip9[A any, B any, C any, D any, E any, F any, G any, H any, I any](a []A, b []B, c []C, d []D, e []E, f []F, g []G, h []H, i []I) []Tuple9[A, B, C, D, E, F, G, H, I] { - size := Max[int]([]int{len(a), len(b), len(c), len(d), len(e), len(f), len(g), len(h), len(i)}) + size := Max([]int{len(a), len(b), len(c), len(d), len(e), len(f), len(g), len(h), len(i)}) result := make([]Tuple9[A, B, C, D, E, F, G, H, I], 0, size) for index := 0; index < size; index++ { - _a, _ := Nth[A](a, index) - _b, _ := Nth[B](b, index) - _c, _ := Nth[C](c, index) - _d, _ := Nth[D](d, index) - _e, _ := Nth[E](e, index) - _f, _ := Nth[F](f, index) - _g, _ := Nth[G](g, index) - _h, _ := Nth[H](h, index) - _i, _ := Nth[I](i, index) + _a, _ := Nth(a, index) + _b, _ := Nth(b, index) + _c, _ := Nth(c, index) + _d, _ := Nth(d, index) + _e, _ := Nth(e, index) + _f, _ := Nth(f, index) + _g, _ := Nth(g, index) + _h, _ := Nth(h, index) + _i, _ := Nth(i, index) result = append(result, Tuple9[A, B, C, D, E, F, G, H, I]{ A: _a, @@ -306,6 +330,7 @@ func Zip9[A any, B any, C any, D any, E any, F any, G any, H any, I any](a []A, // Unzip2 accepts an array of grouped elements and creates an array regrouping the elements // to their pre-zip configuration. +// Play: https://go.dev/play/p/ciHugugvaAW func Unzip2[A any, B any](tuples []Tuple2[A, B]) ([]A, []B) { size := len(tuples) r1 := make([]A, 0, size) @@ -321,6 +346,7 @@ func Unzip2[A any, B any](tuples []Tuple2[A, B]) ([]A, []B) { // Unzip3 accepts an array of grouped elements and creates an array regrouping the elements // to their pre-zip configuration. +// Play: https://go.dev/play/p/ciHugugvaAW func Unzip3[A any, B any, C any](tuples []Tuple3[A, B, C]) ([]A, []B, []C) { size := len(tuples) r1 := make([]A, 0, size) @@ -338,6 +364,7 @@ func Unzip3[A any, B any, C any](tuples []Tuple3[A, B, C]) ([]A, []B, []C) { // Unzip4 accepts an array of grouped elements and creates an array regrouping the elements // to their pre-zip configuration. +// Play: https://go.dev/play/p/ciHugugvaAW func Unzip4[A any, B any, C any, D any](tuples []Tuple4[A, B, C, D]) ([]A, []B, []C, []D) { size := len(tuples) r1 := make([]A, 0, size) @@ -357,6 +384,7 @@ func Unzip4[A any, B any, C any, D any](tuples []Tuple4[A, B, C, D]) ([]A, []B, // Unzip5 accepts an array of grouped elements and creates an array regrouping the elements // to their pre-zip configuration. +// Play: https://go.dev/play/p/ciHugugvaAW func Unzip5[A any, B any, C any, D any, E any](tuples []Tuple5[A, B, C, D, E]) ([]A, []B, []C, []D, []E) { size := len(tuples) r1 := make([]A, 0, size) @@ -378,6 +406,7 @@ func Unzip5[A any, B any, C any, D any, E any](tuples []Tuple5[A, B, C, D, E]) ( // Unzip6 accepts an array of grouped elements and creates an array regrouping the elements // to their pre-zip configuration. +// Play: https://go.dev/play/p/ciHugugvaAW func Unzip6[A any, B any, C any, D any, E any, F any](tuples []Tuple6[A, B, C, D, E, F]) ([]A, []B, []C, []D, []E, []F) { size := len(tuples) r1 := make([]A, 0, size) @@ -401,6 +430,7 @@ func Unzip6[A any, B any, C any, D any, E any, F any](tuples []Tuple6[A, B, C, D // Unzip7 accepts an array of grouped elements and creates an array regrouping the elements // to their pre-zip configuration. +// Play: https://go.dev/play/p/ciHugugvaAW func Unzip7[A any, B any, C any, D any, E any, F any, G any](tuples []Tuple7[A, B, C, D, E, F, G]) ([]A, []B, []C, []D, []E, []F, []G) { size := len(tuples) r1 := make([]A, 0, size) @@ -426,6 +456,7 @@ func Unzip7[A any, B any, C any, D any, E any, F any, G any](tuples []Tuple7[A, // Unzip8 accepts an array of grouped elements and creates an array regrouping the elements // to their pre-zip configuration. +// Play: https://go.dev/play/p/ciHugugvaAW func Unzip8[A any, B any, C any, D any, E any, F any, G any, H any](tuples []Tuple8[A, B, C, D, E, F, G, H]) ([]A, []B, []C, []D, []E, []F, []G, []H) { size := len(tuples) r1 := make([]A, 0, size) @@ -453,6 +484,7 @@ func Unzip8[A any, B any, C any, D any, E any, F any, G any, H any](tuples []Tup // Unzip9 accepts an array of grouped elements and creates an array regrouping the elements // to their pre-zip configuration. +// Play: https://go.dev/play/p/ciHugugvaAW func Unzip9[A any, B any, C any, D any, E any, F any, G any, H any, I any](tuples []Tuple9[A, B, C, D, E, F, G, H, I]) ([]A, []B, []C, []D, []E, []F, []G, []H, []I) { size := len(tuples) r1 := make([]A, 0, size) diff --git a/vendor/github.com/samber/lo/type_manipulation.go b/vendor/github.com/samber/lo/type_manipulation.go new file mode 100644 index 00000000..fe99ee1f --- /dev/null +++ b/vendor/github.com/samber/lo/type_manipulation.go @@ -0,0 +1,88 @@ +package lo + +// ToPtr returns a pointer copy of value. +func ToPtr[T any](x T) *T { + return &x +} + +// FromPtr returns the pointer value or empty. +func FromPtr[T any](x *T) T { + if x == nil { + return Empty[T]() + } + + return *x +} + +// FromPtrOr returns the pointer value or the fallback value. +func FromPtrOr[T any](x *T, fallback T) T { + if x == nil { + return fallback + } + + return *x +} + +// ToSlicePtr returns a slice of pointer copy of value. +func ToSlicePtr[T any](collection []T) []*T { + return Map(collection, func(x T, _ int) *T { + return &x + }) +} + +// ToAnySlice returns a slice with all elements mapped to `any` type +func ToAnySlice[T any](collection []T) []any { + result := make([]any, len(collection)) + for i, item := range collection { + result[i] = item + } + return result +} + +// FromAnySlice returns an `any` slice with all elements mapped to a type. +// Returns false in case of type conversion failure. +func FromAnySlice[T any](in []any) (out []T, ok bool) { + defer func() { + if r := recover(); r != nil { + out = []T{} + ok = false + } + }() + + result := make([]T, len(in)) + for i, item := range in { + result[i] = item.(T) + } + return result, true +} + +// Empty returns an empty value. +func Empty[T any]() T { + var zero T + return zero +} + +// IsEmpty returns true if argument is a zero value. +func IsEmpty[T comparable](v T) bool { + var zero T + return zero == v +} + +// IsNotEmpty returns true if argument is not a zero value. +func IsNotEmpty[T comparable](v T) bool { + var zero T + return zero != v +} + +// Coalesce returns the first non-empty arguments. Arguments must be comparable. +func Coalesce[T comparable](v ...T) (result T, ok bool) { + for _, e := range v { + if e != result { + result = e + ok = true + return + } + } + + return +} diff --git a/vendor/github.com/stretchr/testify/assert/assertion_compare.go b/vendor/github.com/stretchr/testify/assert/assertion_compare.go index 41649d26..95d8e59d 100644 --- a/vendor/github.com/stretchr/testify/assert/assertion_compare.go +++ b/vendor/github.com/stretchr/testify/assert/assertion_compare.go @@ -1,8 +1,10 @@ package assert import ( + "bytes" "fmt" "reflect" + "time" ) type CompareType int @@ -30,6 +32,9 @@ var ( float64Type = reflect.TypeOf(float64(1)) stringType = reflect.TypeOf("") + + timeType = reflect.TypeOf(time.Time{}) + bytesType = reflect.TypeOf([]byte{}) ) func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) { @@ -299,6 +304,47 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) { return compareLess, true } } + // Check for known struct types we can check for compare results. + case reflect.Struct: + { + // All structs enter here. We're not interested in most types. + if !canConvert(obj1Value, timeType) { + break + } + + // time.Time can compared! + timeObj1, ok := obj1.(time.Time) + if !ok { + timeObj1 = obj1Value.Convert(timeType).Interface().(time.Time) + } + + timeObj2, ok := obj2.(time.Time) + if !ok { + timeObj2 = obj2Value.Convert(timeType).Interface().(time.Time) + } + + return compare(timeObj1.UnixNano(), timeObj2.UnixNano(), reflect.Int64) + } + case reflect.Slice: + { + // We only care about the []byte type. + if !canConvert(obj1Value, bytesType) { + break + } + + // []byte can be compared! + bytesObj1, ok := obj1.([]byte) + if !ok { + bytesObj1 = obj1Value.Convert(bytesType).Interface().([]byte) + + } + bytesObj2, ok := obj2.([]byte) + if !ok { + bytesObj2 = obj2Value.Convert(bytesType).Interface().([]byte) + } + + return CompareType(bytes.Compare(bytesObj1, bytesObj2)), true + } } return compareEqual, false @@ -310,7 +356,10 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) { // assert.Greater(t, float64(2), float64(1)) // assert.Greater(t, "b", "a") func Greater(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) bool { - return compareTwoValues(t, e1, e2, []CompareType{compareGreater}, "\"%v\" is not greater than \"%v\"", msgAndArgs) + if h, ok := t.(tHelper); ok { + h.Helper() + } + return compareTwoValues(t, e1, e2, []CompareType{compareGreater}, "\"%v\" is not greater than \"%v\"", msgAndArgs...) } // GreaterOrEqual asserts that the first element is greater than or equal to the second @@ -320,7 +369,10 @@ func Greater(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface // assert.GreaterOrEqual(t, "b", "a") // assert.GreaterOrEqual(t, "b", "b") func GreaterOrEqual(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) bool { - return compareTwoValues(t, e1, e2, []CompareType{compareGreater, compareEqual}, "\"%v\" is not greater than or equal to \"%v\"", msgAndArgs) + if h, ok := t.(tHelper); ok { + h.Helper() + } + return compareTwoValues(t, e1, e2, []CompareType{compareGreater, compareEqual}, "\"%v\" is not greater than or equal to \"%v\"", msgAndArgs...) } // Less asserts that the first element is less than the second @@ -329,7 +381,10 @@ func GreaterOrEqual(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...in // assert.Less(t, float64(1), float64(2)) // assert.Less(t, "a", "b") func Less(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) bool { - return compareTwoValues(t, e1, e2, []CompareType{compareLess}, "\"%v\" is not less than \"%v\"", msgAndArgs) + if h, ok := t.(tHelper); ok { + h.Helper() + } + return compareTwoValues(t, e1, e2, []CompareType{compareLess}, "\"%v\" is not less than \"%v\"", msgAndArgs...) } // LessOrEqual asserts that the first element is less than or equal to the second @@ -339,7 +394,10 @@ func Less(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) // assert.LessOrEqual(t, "a", "b") // assert.LessOrEqual(t, "b", "b") func LessOrEqual(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) bool { - return compareTwoValues(t, e1, e2, []CompareType{compareLess, compareEqual}, "\"%v\" is not less than or equal to \"%v\"", msgAndArgs) + if h, ok := t.(tHelper); ok { + h.Helper() + } + return compareTwoValues(t, e1, e2, []CompareType{compareLess, compareEqual}, "\"%v\" is not less than or equal to \"%v\"", msgAndArgs...) } // Positive asserts that the specified element is positive @@ -347,8 +405,11 @@ func LessOrEqual(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...inter // assert.Positive(t, 1) // assert.Positive(t, 1.23) func Positive(t TestingT, e interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } zero := reflect.Zero(reflect.TypeOf(e)) - return compareTwoValues(t, e, zero.Interface(), []CompareType{compareGreater}, "\"%v\" is not positive", msgAndArgs) + return compareTwoValues(t, e, zero.Interface(), []CompareType{compareGreater}, "\"%v\" is not positive", msgAndArgs...) } // Negative asserts that the specified element is negative @@ -356,8 +417,11 @@ func Positive(t TestingT, e interface{}, msgAndArgs ...interface{}) bool { // assert.Negative(t, -1) // assert.Negative(t, -1.23) func Negative(t TestingT, e interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } zero := reflect.Zero(reflect.TypeOf(e)) - return compareTwoValues(t, e, zero.Interface(), []CompareType{compareLess}, "\"%v\" is not negative", msgAndArgs) + return compareTwoValues(t, e, zero.Interface(), []CompareType{compareLess}, "\"%v\" is not negative", msgAndArgs...) } func compareTwoValues(t TestingT, e1 interface{}, e2 interface{}, allowedComparesResults []CompareType, failMessage string, msgAndArgs ...interface{}) bool { diff --git a/vendor/github.com/stretchr/testify/assert/assertion_compare_can_convert.go b/vendor/github.com/stretchr/testify/assert/assertion_compare_can_convert.go new file mode 100644 index 00000000..da867903 --- /dev/null +++ b/vendor/github.com/stretchr/testify/assert/assertion_compare_can_convert.go @@ -0,0 +1,16 @@ +//go:build go1.17 +// +build go1.17 + +// TODO: once support for Go 1.16 is dropped, this file can be +// merged/removed with assertion_compare_go1.17_test.go and +// assertion_compare_legacy.go + +package assert + +import "reflect" + +// Wrapper around reflect.Value.CanConvert, for compatibility +// reasons. +func canConvert(value reflect.Value, to reflect.Type) bool { + return value.CanConvert(to) +} diff --git a/vendor/github.com/stretchr/testify/assert/assertion_compare_legacy.go b/vendor/github.com/stretchr/testify/assert/assertion_compare_legacy.go new file mode 100644 index 00000000..1701af2a --- /dev/null +++ b/vendor/github.com/stretchr/testify/assert/assertion_compare_legacy.go @@ -0,0 +1,16 @@ +//go:build !go1.17 +// +build !go1.17 + +// TODO: once support for Go 1.16 is dropped, this file can be +// merged/removed with assertion_compare_go1.17_test.go and +// assertion_compare_can_convert.go + +package assert + +import "reflect" + +// Older versions of Go does not have the reflect.Value.CanConvert +// method. +func canConvert(value reflect.Value, to reflect.Type) bool { + return false +} diff --git a/vendor/github.com/stretchr/testify/assert/assertion_format.go b/vendor/github.com/stretchr/testify/assert/assertion_format.go index 4dfd1229..7880b8f9 100644 --- a/vendor/github.com/stretchr/testify/assert/assertion_format.go +++ b/vendor/github.com/stretchr/testify/assert/assertion_format.go @@ -123,6 +123,18 @@ func ErrorAsf(t TestingT, err error, target interface{}, msg string, args ...int return ErrorAs(t, err, target, append([]interface{}{msg}, args...)...) } +// ErrorContainsf asserts that a function returned an error (i.e. not `nil`) +// and that the error contains the specified substring. +// +// actualObj, err := SomeFunction() +// assert.ErrorContainsf(t, err, expectedErrorSubString, "error message %s", "formatted") +func ErrorContainsf(t TestingT, theError error, contains string, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return ErrorContains(t, theError, contains, append([]interface{}{msg}, args...)...) +} + // ErrorIsf asserts that at least one of the errors in err's chain matches target. // This is a wrapper for errors.Is. func ErrorIsf(t TestingT, err error, target error, msg string, args ...interface{}) bool { @@ -724,6 +736,16 @@ func WithinDurationf(t TestingT, expected time.Time, actual time.Time, delta tim return WithinDuration(t, expected, actual, delta, append([]interface{}{msg}, args...)...) } +// WithinRangef asserts that a time is within a time range (inclusive). +// +// assert.WithinRangef(t, time.Now(), time.Now().Add(-time.Second), time.Now().Add(time.Second), "error message %s", "formatted") +func WithinRangef(t TestingT, actual time.Time, start time.Time, end time.Time, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return WithinRange(t, actual, start, end, append([]interface{}{msg}, args...)...) +} + // YAMLEqf asserts that two YAML strings are equivalent. func YAMLEqf(t TestingT, expected string, actual string, msg string, args ...interface{}) bool { if h, ok := t.(tHelper); ok { diff --git a/vendor/github.com/stretchr/testify/assert/assertion_forward.go b/vendor/github.com/stretchr/testify/assert/assertion_forward.go index 25337a6f..339515b8 100644 --- a/vendor/github.com/stretchr/testify/assert/assertion_forward.go +++ b/vendor/github.com/stretchr/testify/assert/assertion_forward.go @@ -222,6 +222,30 @@ func (a *Assertions) ErrorAsf(err error, target interface{}, msg string, args .. return ErrorAsf(a.t, err, target, msg, args...) } +// ErrorContains asserts that a function returned an error (i.e. not `nil`) +// and that the error contains the specified substring. +// +// actualObj, err := SomeFunction() +// a.ErrorContains(err, expectedErrorSubString) +func (a *Assertions) ErrorContains(theError error, contains string, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return ErrorContains(a.t, theError, contains, msgAndArgs...) +} + +// ErrorContainsf asserts that a function returned an error (i.e. not `nil`) +// and that the error contains the specified substring. +// +// actualObj, err := SomeFunction() +// a.ErrorContainsf(err, expectedErrorSubString, "error message %s", "formatted") +func (a *Assertions) ErrorContainsf(theError error, contains string, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return ErrorContainsf(a.t, theError, contains, msg, args...) +} + // ErrorIs asserts that at least one of the errors in err's chain matches target. // This is a wrapper for errors.Is. func (a *Assertions) ErrorIs(err error, target error, msgAndArgs ...interface{}) bool { @@ -1437,6 +1461,26 @@ func (a *Assertions) WithinDurationf(expected time.Time, actual time.Time, delta return WithinDurationf(a.t, expected, actual, delta, msg, args...) } +// WithinRange asserts that a time is within a time range (inclusive). +// +// a.WithinRange(time.Now(), time.Now().Add(-time.Second), time.Now().Add(time.Second)) +func (a *Assertions) WithinRange(actual time.Time, start time.Time, end time.Time, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return WithinRange(a.t, actual, start, end, msgAndArgs...) +} + +// WithinRangef asserts that a time is within a time range (inclusive). +// +// a.WithinRangef(time.Now(), time.Now().Add(-time.Second), time.Now().Add(time.Second), "error message %s", "formatted") +func (a *Assertions) WithinRangef(actual time.Time, start time.Time, end time.Time, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return WithinRangef(a.t, actual, start, end, msg, args...) +} + // YAMLEq asserts that two YAML strings are equivalent. func (a *Assertions) YAMLEq(expected string, actual string, msgAndArgs ...interface{}) bool { if h, ok := a.t.(tHelper); ok { diff --git a/vendor/github.com/stretchr/testify/assert/assertion_order.go b/vendor/github.com/stretchr/testify/assert/assertion_order.go index 1c3b4718..75944878 100644 --- a/vendor/github.com/stretchr/testify/assert/assertion_order.go +++ b/vendor/github.com/stretchr/testify/assert/assertion_order.go @@ -50,7 +50,7 @@ func isOrdered(t TestingT, object interface{}, allowedComparesResults []CompareT // assert.IsIncreasing(t, []float{1, 2}) // assert.IsIncreasing(t, []string{"a", "b"}) func IsIncreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { - return isOrdered(t, object, []CompareType{compareLess}, "\"%v\" is not less than \"%v\"", msgAndArgs) + return isOrdered(t, object, []CompareType{compareLess}, "\"%v\" is not less than \"%v\"", msgAndArgs...) } // IsNonIncreasing asserts that the collection is not increasing @@ -59,7 +59,7 @@ func IsIncreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) boo // assert.IsNonIncreasing(t, []float{2, 1}) // assert.IsNonIncreasing(t, []string{"b", "a"}) func IsNonIncreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { - return isOrdered(t, object, []CompareType{compareEqual, compareGreater}, "\"%v\" is not greater than or equal to \"%v\"", msgAndArgs) + return isOrdered(t, object, []CompareType{compareEqual, compareGreater}, "\"%v\" is not greater than or equal to \"%v\"", msgAndArgs...) } // IsDecreasing asserts that the collection is decreasing @@ -68,7 +68,7 @@ func IsNonIncreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) // assert.IsDecreasing(t, []float{2, 1}) // assert.IsDecreasing(t, []string{"b", "a"}) func IsDecreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { - return isOrdered(t, object, []CompareType{compareGreater}, "\"%v\" is not greater than \"%v\"", msgAndArgs) + return isOrdered(t, object, []CompareType{compareGreater}, "\"%v\" is not greater than \"%v\"", msgAndArgs...) } // IsNonDecreasing asserts that the collection is not decreasing @@ -77,5 +77,5 @@ func IsDecreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) boo // assert.IsNonDecreasing(t, []float{1, 2}) // assert.IsNonDecreasing(t, []string{"a", "b"}) func IsNonDecreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { - return isOrdered(t, object, []CompareType{compareLess, compareEqual}, "\"%v\" is not less than or equal to \"%v\"", msgAndArgs) + return isOrdered(t, object, []CompareType{compareLess, compareEqual}, "\"%v\" is not less than or equal to \"%v\"", msgAndArgs...) } diff --git a/vendor/github.com/stretchr/testify/assert/assertions.go b/vendor/github.com/stretchr/testify/assert/assertions.go index bcac4401..fa1245b1 100644 --- a/vendor/github.com/stretchr/testify/assert/assertions.go +++ b/vendor/github.com/stretchr/testify/assert/assertions.go @@ -8,6 +8,7 @@ import ( "fmt" "math" "os" + "path/filepath" "reflect" "regexp" "runtime" @@ -144,7 +145,8 @@ func CallerInfo() []string { if len(parts) > 1 { dir := parts[len(parts)-2] if (dir != "assert" && dir != "mock" && dir != "require") || file == "mock_test.go" { - callers = append(callers, fmt.Sprintf("%s:%d", file, line)) + path, _ := filepath.Abs(file) + callers = append(callers, fmt.Sprintf("%s:%d", path, line)) } } @@ -563,16 +565,17 @@ func isEmpty(object interface{}) bool { switch objValue.Kind() { // collection types are empty when they have no element - case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice: + case reflect.Chan, reflect.Map, reflect.Slice: return objValue.Len() == 0 - // pointers are empty if nil or if the value they point to is empty + // pointers are empty if nil or if the value they point to is empty case reflect.Ptr: if objValue.IsNil() { return true } deref := objValue.Elem().Interface() return isEmpty(deref) - // for all other types, compare against the zero value + // for all other types, compare against the zero value + // array types are empty when they match their zero-initialized state default: zero := reflect.Zero(objValue.Type()) return reflect.DeepEqual(object, zero.Interface()) @@ -718,10 +721,14 @@ func NotEqualValues(t TestingT, expected, actual interface{}, msgAndArgs ...inte // return (false, false) if impossible. // return (true, false) if element was not found. // return (true, true) if element was found. -func includeElement(list interface{}, element interface{}) (ok, found bool) { +func containsElement(list interface{}, element interface{}) (ok, found bool) { listValue := reflect.ValueOf(list) - listKind := reflect.TypeOf(list).Kind() + listType := reflect.TypeOf(list) + if listType == nil { + return false, false + } + listKind := listType.Kind() defer func() { if e := recover(); e != nil { ok = false @@ -764,7 +771,7 @@ func Contains(t TestingT, s, contains interface{}, msgAndArgs ...interface{}) bo h.Helper() } - ok, found := includeElement(s, contains) + ok, found := containsElement(s, contains) if !ok { return Fail(t, fmt.Sprintf("%#v could not be applied builtin len()", s), msgAndArgs...) } @@ -787,7 +794,7 @@ func NotContains(t TestingT, s, contains interface{}, msgAndArgs ...interface{}) h.Helper() } - ok, found := includeElement(s, contains) + ok, found := containsElement(s, contains) if !ok { return Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", s), msgAndArgs...) } @@ -811,7 +818,6 @@ func Subset(t TestingT, list, subset interface{}, msgAndArgs ...interface{}) (ok return true // we consider nil to be equal to the nil set } - subsetValue := reflect.ValueOf(subset) defer func() { if e := recover(); e != nil { ok = false @@ -821,17 +827,35 @@ func Subset(t TestingT, list, subset interface{}, msgAndArgs ...interface{}) (ok listKind := reflect.TypeOf(list).Kind() subsetKind := reflect.TypeOf(subset).Kind() - if listKind != reflect.Array && listKind != reflect.Slice { + if listKind != reflect.Array && listKind != reflect.Slice && listKind != reflect.Map { return Fail(t, fmt.Sprintf("%q has an unsupported type %s", list, listKind), msgAndArgs...) } - if subsetKind != reflect.Array && subsetKind != reflect.Slice { + if subsetKind != reflect.Array && subsetKind != reflect.Slice && listKind != reflect.Map { return Fail(t, fmt.Sprintf("%q has an unsupported type %s", subset, subsetKind), msgAndArgs...) } + subsetValue := reflect.ValueOf(subset) + if subsetKind == reflect.Map && listKind == reflect.Map { + listValue := reflect.ValueOf(list) + subsetKeys := subsetValue.MapKeys() + + for i := 0; i < len(subsetKeys); i++ { + subsetKey := subsetKeys[i] + subsetElement := subsetValue.MapIndex(subsetKey).Interface() + listElement := listValue.MapIndex(subsetKey).Interface() + + if !ObjectsAreEqual(subsetElement, listElement) { + return Fail(t, fmt.Sprintf("\"%s\" does not contain \"%s\"", list, subsetElement), msgAndArgs...) + } + } + + return true + } + for i := 0; i < subsetValue.Len(); i++ { element := subsetValue.Index(i).Interface() - ok, found := includeElement(list, element) + ok, found := containsElement(list, element) if !ok { return Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", list), msgAndArgs...) } @@ -852,10 +876,9 @@ func NotSubset(t TestingT, list, subset interface{}, msgAndArgs ...interface{}) h.Helper() } if subset == nil { - return Fail(t, fmt.Sprintf("nil is the empty set which is a subset of every set"), msgAndArgs...) + return Fail(t, "nil is the empty set which is a subset of every set", msgAndArgs...) } - subsetValue := reflect.ValueOf(subset) defer func() { if e := recover(); e != nil { ok = false @@ -865,17 +888,35 @@ func NotSubset(t TestingT, list, subset interface{}, msgAndArgs ...interface{}) listKind := reflect.TypeOf(list).Kind() subsetKind := reflect.TypeOf(subset).Kind() - if listKind != reflect.Array && listKind != reflect.Slice { + if listKind != reflect.Array && listKind != reflect.Slice && listKind != reflect.Map { return Fail(t, fmt.Sprintf("%q has an unsupported type %s", list, listKind), msgAndArgs...) } - if subsetKind != reflect.Array && subsetKind != reflect.Slice { + if subsetKind != reflect.Array && subsetKind != reflect.Slice && listKind != reflect.Map { return Fail(t, fmt.Sprintf("%q has an unsupported type %s", subset, subsetKind), msgAndArgs...) } + subsetValue := reflect.ValueOf(subset) + if subsetKind == reflect.Map && listKind == reflect.Map { + listValue := reflect.ValueOf(list) + subsetKeys := subsetValue.MapKeys() + + for i := 0; i < len(subsetKeys); i++ { + subsetKey := subsetKeys[i] + subsetElement := subsetValue.MapIndex(subsetKey).Interface() + listElement := listValue.MapIndex(subsetKey).Interface() + + if !ObjectsAreEqual(subsetElement, listElement) { + return true + } + } + + return Fail(t, fmt.Sprintf("%q is a subset of %q", subset, list), msgAndArgs...) + } + for i := 0; i < subsetValue.Len(); i++ { element := subsetValue.Index(i).Interface() - ok, found := includeElement(list, element) + ok, found := containsElement(list, element) if !ok { return Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", list), msgAndArgs...) } @@ -1000,27 +1041,21 @@ func Condition(t TestingT, comp Comparison, msgAndArgs ...interface{}) bool { type PanicTestFunc func() // didPanic returns true if the function passed to it panics. Otherwise, it returns false. -func didPanic(f PanicTestFunc) (bool, interface{}, string) { - - didPanic := false - var message interface{} - var stack string - func() { - - defer func() { - if message = recover(); message != nil { - didPanic = true - stack = string(debug.Stack()) - } - }() - - // call the target function - f() +func didPanic(f PanicTestFunc) (didPanic bool, message interface{}, stack string) { + didPanic = true + defer func() { + message = recover() + if didPanic { + stack = string(debug.Stack()) + } }() - return didPanic, message, stack + // call the target function + f() + didPanic = false + return } // Panics asserts that the code inside the specified PanicTestFunc panics. @@ -1111,6 +1146,27 @@ func WithinDuration(t TestingT, expected, actual time.Time, delta time.Duration, return true } +// WithinRange asserts that a time is within a time range (inclusive). +// +// assert.WithinRange(t, time.Now(), time.Now().Add(-time.Second), time.Now().Add(time.Second)) +func WithinRange(t TestingT, actual, start, end time.Time, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + if end.Before(start) { + return Fail(t, "Start should be before end", msgAndArgs...) + } + + if actual.Before(start) { + return Fail(t, fmt.Sprintf("Time %v expected to be in time range %v to %v, but is before the range", actual, start, end), msgAndArgs...) + } else if actual.After(end) { + return Fail(t, fmt.Sprintf("Time %v expected to be in time range %v to %v, but is after the range", actual, start, end), msgAndArgs...) + } + + return true +} + func toFloat(x interface{}) (float64, bool) { var xf float64 xok := true @@ -1161,11 +1217,15 @@ func InDelta(t TestingT, expected, actual interface{}, delta float64, msgAndArgs bf, bok := toFloat(actual) if !aok || !bok { - return Fail(t, fmt.Sprintf("Parameters must be numerical"), msgAndArgs...) + return Fail(t, "Parameters must be numerical", msgAndArgs...) + } + + if math.IsNaN(af) && math.IsNaN(bf) { + return true } if math.IsNaN(af) { - return Fail(t, fmt.Sprintf("Expected must not be NaN"), msgAndArgs...) + return Fail(t, "Expected must not be NaN", msgAndArgs...) } if math.IsNaN(bf) { @@ -1188,7 +1248,7 @@ func InDeltaSlice(t TestingT, expected, actual interface{}, delta float64, msgAn if expected == nil || actual == nil || reflect.TypeOf(actual).Kind() != reflect.Slice || reflect.TypeOf(expected).Kind() != reflect.Slice { - return Fail(t, fmt.Sprintf("Parameters must be slice"), msgAndArgs...) + return Fail(t, "Parameters must be slice", msgAndArgs...) } actualSlice := reflect.ValueOf(actual) @@ -1250,8 +1310,12 @@ func InDeltaMapValues(t TestingT, expected, actual interface{}, delta float64, m func calcRelativeError(expected, actual interface{}) (float64, error) { af, aok := toFloat(expected) - if !aok { - return 0, fmt.Errorf("expected value %q cannot be converted to float", expected) + bf, bok := toFloat(actual) + if !aok || !bok { + return 0, fmt.Errorf("Parameters must be numerical") + } + if math.IsNaN(af) && math.IsNaN(bf) { + return 0, nil } if math.IsNaN(af) { return 0, errors.New("expected value must not be NaN") @@ -1259,10 +1323,6 @@ func calcRelativeError(expected, actual interface{}) (float64, error) { if af == 0 { return 0, fmt.Errorf("expected value must have a value other than zero to calculate the relative error") } - bf, bok := toFloat(actual) - if !bok { - return 0, fmt.Errorf("actual value %q cannot be converted to float", actual) - } if math.IsNaN(bf) { return 0, errors.New("actual value must not be NaN") } @@ -1298,7 +1358,7 @@ func InEpsilonSlice(t TestingT, expected, actual interface{}, epsilon float64, m if expected == nil || actual == nil || reflect.TypeOf(actual).Kind() != reflect.Slice || reflect.TypeOf(expected).Kind() != reflect.Slice { - return Fail(t, fmt.Sprintf("Parameters must be slice"), msgAndArgs...) + return Fail(t, "Parameters must be slice", msgAndArgs...) } actualSlice := reflect.ValueOf(actual) @@ -1375,6 +1435,27 @@ func EqualError(t TestingT, theError error, errString string, msgAndArgs ...inte return true } +// ErrorContains asserts that a function returned an error (i.e. not `nil`) +// and that the error contains the specified substring. +// +// actualObj, err := SomeFunction() +// assert.ErrorContains(t, err, expectedErrorSubString) +func ErrorContains(t TestingT, theError error, contains string, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if !Error(t, theError, msgAndArgs...) { + return false + } + + actual := theError.Error() + if !strings.Contains(actual, contains) { + return Fail(t, fmt.Sprintf("Error %#v does not contain %#v", actual, contains), msgAndArgs...) + } + + return true +} + // matchRegexp return true if a specified regexp matches a string. func matchRegexp(rx interface{}, str interface{}) bool { @@ -1588,12 +1669,17 @@ func diff(expected interface{}, actual interface{}) string { } var e, a string - if et != reflect.TypeOf("") { - e = spewConfig.Sdump(expected) - a = spewConfig.Sdump(actual) - } else { + + switch et { + case reflect.TypeOf(""): e = reflect.ValueOf(expected).String() a = reflect.ValueOf(actual).String() + case reflect.TypeOf(time.Time{}): + e = spewConfigStringerEnabled.Sdump(expected) + a = spewConfigStringerEnabled.Sdump(actual) + default: + e = spewConfig.Sdump(expected) + a = spewConfig.Sdump(actual) } diff, _ := difflib.GetUnifiedDiffString(difflib.UnifiedDiff{ @@ -1625,6 +1711,14 @@ var spewConfig = spew.ConfigState{ MaxDepth: 10, } +var spewConfigStringerEnabled = spew.ConfigState{ + Indent: " ", + DisablePointerAddresses: true, + DisableCapacities: true, + SortKeys: true, + MaxDepth: 10, +} + type tHelper interface { Helper() } diff --git a/vendor/gopkg.in/yaml.v3/decode.go b/vendor/gopkg.in/yaml.v3/decode.go index df36e3a3..0173b698 100644 --- a/vendor/gopkg.in/yaml.v3/decode.go +++ b/vendor/gopkg.in/yaml.v3/decode.go @@ -100,7 +100,10 @@ func (p *parser) peek() yaml_event_type_t { if p.event.typ != yaml_NO_EVENT { return p.event.typ } - if !yaml_parser_parse(&p.parser, &p.event) { + // It's curious choice from the underlying API to generally return a + // positive result on success, but on this case return true in an error + // scenario. This was the source of bugs in the past (issue #666). + if !yaml_parser_parse(&p.parser, &p.event) || p.parser.error != yaml_NO_ERROR { p.fail() } return p.event.typ @@ -320,6 +323,8 @@ type decoder struct { decodeCount int aliasCount int aliasDepth int + + mergedFields map[interface{}]bool } var ( @@ -808,6 +813,11 @@ func (d *decoder) mapping(n *Node, out reflect.Value) (good bool) { } } + mergedFields := d.mergedFields + d.mergedFields = nil + + var mergeNode *Node + mapIsNew := false if out.IsNil() { out.Set(reflect.MakeMap(outt)) @@ -815,11 +825,18 @@ func (d *decoder) mapping(n *Node, out reflect.Value) (good bool) { } for i := 0; i < l; i += 2 { if isMerge(n.Content[i]) { - d.merge(n.Content[i+1], out) + mergeNode = n.Content[i+1] continue } k := reflect.New(kt).Elem() if d.unmarshal(n.Content[i], k) { + if mergedFields != nil { + ki := k.Interface() + if mergedFields[ki] { + continue + } + mergedFields[ki] = true + } kkind := k.Kind() if kkind == reflect.Interface { kkind = k.Elem().Kind() @@ -833,6 +850,12 @@ func (d *decoder) mapping(n *Node, out reflect.Value) (good bool) { } } } + + d.mergedFields = mergedFields + if mergeNode != nil { + d.merge(n, mergeNode, out) + } + d.stringMapType = stringMapType d.generalMapType = generalMapType return true @@ -844,7 +867,8 @@ func isStringMap(n *Node) bool { } l := len(n.Content) for i := 0; i < l; i += 2 { - if n.Content[i].ShortTag() != strTag { + shortTag := n.Content[i].ShortTag() + if shortTag != strTag && shortTag != mergeTag { return false } } @@ -861,7 +885,6 @@ func (d *decoder) mappingStruct(n *Node, out reflect.Value) (good bool) { var elemType reflect.Type if sinfo.InlineMap != -1 { inlineMap = out.Field(sinfo.InlineMap) - inlineMap.Set(reflect.New(inlineMap.Type()).Elem()) elemType = inlineMap.Type().Elem() } @@ -870,6 +893,9 @@ func (d *decoder) mappingStruct(n *Node, out reflect.Value) (good bool) { d.prepare(n, field) } + mergedFields := d.mergedFields + d.mergedFields = nil + var mergeNode *Node var doneFields []bool if d.uniqueKeys { doneFields = make([]bool, len(sinfo.FieldsList)) @@ -879,13 +905,20 @@ func (d *decoder) mappingStruct(n *Node, out reflect.Value) (good bool) { for i := 0; i < l; i += 2 { ni := n.Content[i] if isMerge(ni) { - d.merge(n.Content[i+1], out) + mergeNode = n.Content[i+1] continue } if !d.unmarshal(ni, name) { continue } - if info, ok := sinfo.FieldsMap[name.String()]; ok { + sname := name.String() + if mergedFields != nil { + if mergedFields[sname] { + continue + } + mergedFields[sname] = true + } + if info, ok := sinfo.FieldsMap[sname]; ok { if d.uniqueKeys { if doneFields[info.Id] { d.terrors = append(d.terrors, fmt.Sprintf("line %d: field %s already set in type %s", ni.Line, name.String(), out.Type())) @@ -911,6 +944,11 @@ func (d *decoder) mappingStruct(n *Node, out reflect.Value) (good bool) { d.terrors = append(d.terrors, fmt.Sprintf("line %d: field %s not found in type %s", ni.Line, name.String(), out.Type())) } } + + d.mergedFields = mergedFields + if mergeNode != nil { + d.merge(n, mergeNode, out) + } return true } @@ -918,19 +956,29 @@ func failWantMap() { failf("map merge requires map or sequence of maps as the value") } -func (d *decoder) merge(n *Node, out reflect.Value) { - switch n.Kind { +func (d *decoder) merge(parent *Node, merge *Node, out reflect.Value) { + mergedFields := d.mergedFields + if mergedFields == nil { + d.mergedFields = make(map[interface{}]bool) + for i := 0; i < len(parent.Content); i += 2 { + k := reflect.New(ifaceType).Elem() + if d.unmarshal(parent.Content[i], k) { + d.mergedFields[k.Interface()] = true + } + } + } + + switch merge.Kind { case MappingNode: - d.unmarshal(n, out) + d.unmarshal(merge, out) case AliasNode: - if n.Alias != nil && n.Alias.Kind != MappingNode { + if merge.Alias != nil && merge.Alias.Kind != MappingNode { failWantMap() } - d.unmarshal(n, out) + d.unmarshal(merge, out) case SequenceNode: - // Step backwards as earlier nodes take precedence. - for i := len(n.Content) - 1; i >= 0; i-- { - ni := n.Content[i] + for i := 0; i < len(merge.Content); i++ { + ni := merge.Content[i] if ni.Kind == AliasNode { if ni.Alias != nil && ni.Alias.Kind != MappingNode { failWantMap() @@ -943,6 +991,8 @@ func (d *decoder) merge(n *Node, out reflect.Value) { default: failWantMap() } + + d.mergedFields = mergedFields } func isMerge(n *Node) bool { diff --git a/vendor/gopkg.in/yaml.v3/parserc.go b/vendor/gopkg.in/yaml.v3/parserc.go index ac66fccc..268558a0 100644 --- a/vendor/gopkg.in/yaml.v3/parserc.go +++ b/vendor/gopkg.in/yaml.v3/parserc.go @@ -687,6 +687,9 @@ func yaml_parser_parse_node(parser *yaml_parser_t, event *yaml_event_t, block, i func yaml_parser_parse_block_sequence_entry(parser *yaml_parser_t, event *yaml_event_t, first bool) bool { if first { token := peek_token(parser) + if token == nil { + return false + } parser.marks = append(parser.marks, token.start_mark) skip_token(parser) } @@ -786,7 +789,7 @@ func yaml_parser_split_stem_comment(parser *yaml_parser_t, stem_len int) { } token := peek_token(parser) - if token.typ != yaml_BLOCK_SEQUENCE_START_TOKEN && token.typ != yaml_BLOCK_MAPPING_START_TOKEN { + if token == nil || token.typ != yaml_BLOCK_SEQUENCE_START_TOKEN && token.typ != yaml_BLOCK_MAPPING_START_TOKEN { return } @@ -813,6 +816,9 @@ func yaml_parser_split_stem_comment(parser *yaml_parser_t, stem_len int) { func yaml_parser_parse_block_mapping_key(parser *yaml_parser_t, event *yaml_event_t, first bool) bool { if first { token := peek_token(parser) + if token == nil { + return false + } parser.marks = append(parser.marks, token.start_mark) skip_token(parser) } @@ -922,6 +928,9 @@ func yaml_parser_parse_block_mapping_value(parser *yaml_parser_t, event *yaml_ev func yaml_parser_parse_flow_sequence_entry(parser *yaml_parser_t, event *yaml_event_t, first bool) bool { if first { token := peek_token(parser) + if token == nil { + return false + } parser.marks = append(parser.marks, token.start_mark) skip_token(parser) } diff --git a/vendor/modules.txt b/vendor/modules.txt index 3c9757d8..a8818516 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -121,6 +121,10 @@ github.com/jesseduffield/gocui # github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10 ## explicit; go 1.18 github.com/jesseduffield/kill +# github.com/jesseduffield/lazycore v0.0.0-20221009164442-17c8b878c316 +## explicit; go 1.18 +github.com/jesseduffield/lazycore/pkg/boxlayout +github.com/jesseduffield/lazycore/pkg/utils # github.com/jesseduffield/yaml v0.0.0-20190702115811-b900b7e08b56 ## explicit github.com/jesseduffield/yaml @@ -169,7 +173,7 @@ github.com/pmezard/go-difflib/difflib # github.com/rivo/uniseg v0.2.0 ## explicit; go 1.12 github.com/rivo/uniseg -# github.com/samber/lo v1.20.0 +# github.com/samber/lo v1.31.0 ## explicit; go 1.18 github.com/samber/lo # github.com/sirupsen/logrus v1.4.2 @@ -178,7 +182,7 @@ github.com/sirupsen/logrus # github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad ## explicit github.com/spkg/bom -# github.com/stretchr/testify v1.7.0 +# github.com/stretchr/testify v1.8.0 ## explicit; go 1.13 github.com/stretchr/testify/assert # github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 @@ -213,7 +217,7 @@ golang.org/x/xerrors golang.org/x/xerrors/internal # gopkg.in/yaml.v2 v2.2.2 ## explicit -# gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b +# gopkg.in/yaml.v3 v3.0.1 ## explicit gopkg.in/yaml.v3 # gotest.tools/v3 v3.2.0 From 624e95d5a433a9bd74917c7eb30f4ddf348e426e Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sun, 9 Oct 2022 11:36:51 -0700 Subject: [PATCH 15/42] ensure we never delete views --- pkg/gui/confirmation_panel.go | 59 ++++++++++++++++------------------- pkg/gui/menu_panel.go | 20 +++--------- pkg/gui/view_helpers.go | 3 -- pkg/gui/views.go | 1 + 4 files changed, 33 insertions(+), 50 deletions(-) diff --git a/pkg/gui/confirmation_panel.go b/pkg/gui/confirmation_panel.go index e2df8438..4c3b761a 100644 --- a/pkg/gui/confirmation_panel.go +++ b/pkg/gui/confirmation_panel.go @@ -15,25 +15,27 @@ import ( func (gui *Gui) wrappedConfirmationFunction(function func(*gocui.Gui, *gocui.View) error) func(*gocui.Gui, *gocui.View) error { return func(g *gocui.Gui, v *gocui.View) error { + if err := gui.closeConfirmationPrompt(); err != nil { + return err + } + if function != nil { if err := function(g, v); err != nil { return err } } - return gui.closeConfirmationPrompt() + + return nil } } func (gui *Gui) closeConfirmationPrompt() error { - view, err := gui.g.View("confirmation") - if err != nil { - return nil // if it's already been closed we can just return - } - if err := gui.returnFocus(gui.g, view); err != nil { + if err := gui.returnFocus(gui.g, gui.Views.Confirmation); err != nil { return err } gui.g.DeleteViewKeybindings("confirmation") - return gui.g.DeleteView("confirmation") + gui.Views.Confirmation.Visible = false + return nil } func (gui *Gui) getMessageHeight(wrap bool, message string, width int) int { @@ -62,42 +64,36 @@ func (gui *Gui) getConfirmationPanelDimensions(g *gocui.Gui, wrap bool, prompt s func (gui *Gui) createPromptPanel(g *gocui.Gui, currentView *gocui.View, title string, handleConfirm func(*gocui.Gui, *gocui.View) error) error { gui.onNewPopupPanel() - confirmationView, err := gui.prepareConfirmationPanel(currentView, title, "", false) + err := gui.prepareConfirmationPanel(currentView, title, "", false) if err != nil { return err } - confirmationView.Editable = true + gui.Views.Confirmation.Editable = true return gui.setKeyBindings(g, handleConfirm, nil) } -func (gui *Gui) prepareConfirmationPanel(currentView *gocui.View, title, prompt string, hasLoader bool) (*gocui.View, error) { +func (gui *Gui) prepareConfirmationPanel(currentView *gocui.View, title, prompt string, hasLoader bool) error { x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(gui.g, true, prompt) - confirmationView, err := gui.g.SetView("confirmation", x0, y0, x1, y1, 0) + confirmationView := gui.Views.Confirmation + _, err := gui.g.SetView("confirmation", x0, y0, x1, y1, 0) if err != nil { - if err.Error() != "unknown view" { - return nil, err - } - confirmationView.HasLoader = hasLoader - gui.g.StartTicking() - confirmationView.Title = title - confirmationView.Wrap = true - confirmationView.FgColor = gocui.ColorDefault + return err } + confirmationView.HasLoader = hasLoader + if hasLoader { + gui.g.StartTicking() + } + confirmationView.Title = title + confirmationView.Visible = true gui.g.Update(func(g *gocui.Gui) error { return gui.switchFocus(gui.g, currentView, confirmationView, false) }) - return confirmationView, nil + return nil } func (gui *Gui) onNewPopupPanel() { - viewNames := []string{ - "commitMessage", - "credentials", - "menu", - } - for _, viewName := range viewNames { - _, _ = gui.g.SetViewOnBottom(viewName) - } + gui.Views.Menu.Visible = false + gui.Views.Confirmation.Visible = false } // 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 @@ -110,17 +106,16 @@ func (gui *Gui) createConfirmationPanel(g *gocui.Gui, currentView *gocui.View, t func (gui *Gui) createPopupPanel(g *gocui.Gui, currentView *gocui.View, title, prompt string, hasLoader bool, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error { gui.onNewPopupPanel() g.Update(func(g *gocui.Gui) error { - // delete the existing confirmation panel if it exists - if view, _ := g.View("confirmation"); view != nil { + if gui.currentViewName() == "confirmation" { if err := gui.closeConfirmationPrompt(); err != nil { gui.Log.Error(err.Error()) } } - confirmationView, err := gui.prepareConfirmationPanel(currentView, title, prompt, hasLoader) + err := gui.prepareConfirmationPanel(currentView, title, prompt, hasLoader) if err != nil { return err } - confirmationView.Editable = false + gui.Views.Confirmation.Editable = false if err := gui.renderString(g, "confirmation", prompt); err != nil { return err } diff --git a/pkg/gui/menu_panel.go b/pkg/gui/menu_panel.go index 40bb30ac..adb35818 100644 --- a/pkg/gui/menu_panel.go +++ b/pkg/gui/menu_panel.go @@ -57,10 +57,7 @@ func (gui *Gui) handleMenuClose(g *gocui.Gui, v *gocui.View) error { return err } } - err := g.DeleteView("menu") - if err != nil { - return err - } + gui.Views.Menu.Visible = false return gui.returnFocus(g, v) } @@ -73,7 +70,8 @@ func (gui *Gui) createMenu(title string, items interface{}, itemCount int, handl } x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(gui.g, false, list) - menuView, _ := gui.g.SetView("menu", x0, y0, x1, y1, 0) + _, _ = gui.g.SetView("menu", x0, y0, x1, y1, 0) + menuView := gui.Views.Menu menuView.Title = title menuView.FgColor = gocui.ColorDefault menuView.Clear() @@ -85,11 +83,7 @@ func (gui *Gui) createMenu(title string, items interface{}, itemCount int, handl if err := handlePress(selectedLine); err != nil { return err } - if _, err := gui.g.View("menu"); err == nil { - if _, err := gui.g.SetViewOnBottom("menu"); err != nil { - return err - } - } + menuView.Visible = false return gui.returnFocus(gui.g, menuView) } @@ -105,11 +99,7 @@ func (gui *Gui) createMenu(title string, items interface{}, itemCount int, handl } gui.g.Update(func(g *gocui.Gui) error { - if _, err := gui.g.View("menu"); err == nil { - if _, err := g.SetViewOnTop("menu"); err != nil { - return err - } - } + menuView.Visible = true currentView := gui.g.CurrentView() return gui.switchFocus(gui.g, currentView, menuView, false) }) diff --git a/pkg/gui/view_helpers.go b/pkg/gui/view_helpers.go index 6e1a62cf..a594ccb5 100644 --- a/pkg/gui/view_helpers.go +++ b/pkg/gui/view_helpers.go @@ -142,9 +142,6 @@ func (gui *Gui) switchFocus(g *gocui.Gui, oldView, newView *gocui.View, returnin if _, err := g.SetCurrentView(newView.Name()); err != nil { return err } - if _, err := g.SetViewOnTop(newView.Name()); err != nil { - return err - } g.Cursor = newView.Editable diff --git a/pkg/gui/views.go b/pkg/gui/views.go index 3f83dd29..3de54c26 100644 --- a/pkg/gui/views.go +++ b/pkg/gui/views.go @@ -106,6 +106,7 @@ func (gui *Gui) createAllViews() error { } gui.Views.Confirmation.Visible = false + gui.Views.Confirmation.Wrap = true gui.Views.Menu.Visible = false gui.Views.Limit.Visible = false From 4479b8b29e0f039be0e6eb9e967288407b959c7a Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sun, 9 Oct 2022 16:30:11 -0700 Subject: [PATCH 16/42] fix view stack code --- pkg/gui/arrangement.go | 4 ---- pkg/gui/gui.go | 10 +++++----- pkg/gui/subprocess.go | 4 ++-- pkg/gui/view_helpers.go | 37 ++++++++++++++++++++++++++----------- 4 files changed, 33 insertions(+), 22 deletions(-) diff --git a/pkg/gui/arrangement.go b/pkg/gui/arrangement.go index 58b53706..b24c82af 100644 --- a/pkg/gui/arrangement.go +++ b/pkg/gui/arrangement.go @@ -222,10 +222,6 @@ func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box { // return "files" // default // } -func (gui *Gui) currentNonPopupWindowName() string { - return gui.peekPreviousView() -} - // TODO: do this better. func (gui *Gui) currentSideWindowName() string { windowName := gui.currentWindow() diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index ccafc9ef..17710987 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -8,7 +8,6 @@ import ( "time" "github.com/docker/docker/api/types" - "github.com/golang-collections/collections/stack" // "io" // "io/ioutil" @@ -123,8 +122,9 @@ type panelStates struct { } type guiState struct { - MenuItemCount int // can't store the actual list because it's of interface{} type - PreviousViews *stack.Stack + MenuItemCount int // can't store the actual list because it's of interface{} type + // the names of views in the current focus stack (last item is the current view) + ViewStack []string Platform commands.Platform Panels *panelStates SubProcessOutput string @@ -165,8 +165,8 @@ func NewGui(log *logrus.Entry, dockerCommand *commands.DockerCommand, oSCommand }, Project: &projectState{ContextIndex: 0}, }, - SessionIndex: 0, - PreviousViews: stack.New(), + SessionIndex: 0, + ViewStack: []string{}, } cyclableViews := []string{"project", "containers", "images", "volumes"} diff --git a/pkg/gui/subprocess.go b/pkg/gui/subprocess.go index df23afd6..059f05e1 100644 --- a/pkg/gui/subprocess.go +++ b/pkg/gui/subprocess.go @@ -22,7 +22,7 @@ func (gui *Gui) RunWithSubprocesses() error { break } else if err == gui.Errors.ErrSubProcess { // preparing the state for when we return - gui.pushPreviousView(gui.currentViewName()) + gui.pushView(gui.currentViewName()) // giving goEvery goroutines time to finish gui.State.SessionIndex++ @@ -31,7 +31,7 @@ func (gui *Gui) RunWithSubprocesses() error { } // pop here so we don't stack up view names - gui.popPreviousView() + gui.popView() // ensuring we render e.g. the logs of the currently selected item upon return gui.State.Panels.Main.ObjectKey = "" } else { diff --git a/pkg/gui/view_helpers.go b/pkg/gui/view_helpers.go index a594ccb5..a585d807 100644 --- a/pkg/gui/view_helpers.go +++ b/pkg/gui/view_helpers.go @@ -7,6 +7,7 @@ import ( "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazydocker/pkg/utils" + "github.com/samber/lo" "github.com/spkg/bom" ) @@ -32,7 +33,7 @@ func (gui *Gui) nextView(g *gocui.Gui, v *gocui.View) error { panic(err) } gui.resetMainView() - gui.popPreviousView() + gui.popView() return gui.switchFocus(g, v, focusedView, false) } @@ -58,7 +59,7 @@ func (gui *Gui) previousView(g *gocui.Gui, v *gocui.View) error { panic(err) } gui.resetMainView() - gui.popPreviousView() + gui.popView() return gui.switchFocus(g, v, focusedView, false) } @@ -95,28 +96,42 @@ func (gui *Gui) newLineFocused(v *gocui.View) error { } } -func (gui *Gui) popPreviousView() string { - if gui.State.PreviousViews.Len() > 0 { - return gui.State.PreviousViews.Pop().(string) +func (gui *Gui) popView() string { + if len(gui.State.ViewStack) > 0 { + value := gui.State.ViewStack[len(gui.State.ViewStack)-1] + gui.State.ViewStack = gui.State.ViewStack[:len(gui.State.ViewStack)-1] + return value } return "" } func (gui *Gui) peekPreviousView() string { - if gui.State.PreviousViews.Len() > 0 { - return gui.State.PreviousViews.Peek().(string) + if len(gui.State.ViewStack) > 0 { + return gui.State.ViewStack[len(gui.State.ViewStack)-1] } return "" } -func (gui *Gui) pushPreviousView(name string) { - gui.State.PreviousViews.Push(name) +func (gui *Gui) pushView(name string) { + // No matter what view we're pushing, we first remove all popup panels from the stack + gui.State.ViewStack = lo.Filter(gui.State.ViewStack, func(viewName string, _ int) bool { + return viewName != "confirmation" && viewName != "menu" + }) + + // If we're pushing a side panel, we remove all other panels + if lo.Contains(gui.sideViewNames(), name) { + gui.State.ViewStack = lo.Filter(gui.State.ViewStack, func(viewName string, _ int) bool { + return viewName != "main" + }) + } + + gui.State.ViewStack = append(gui.State.ViewStack, name) } func (gui *Gui) returnFocus(g *gocui.Gui, v *gocui.View) error { - previousViewName := gui.popPreviousView() + previousViewName := gui.popView() previousView, err := g.View(previousViewName) if err != nil { // always fall back to services view if there's no 'previous' view stored @@ -134,7 +149,7 @@ func (gui *Gui) switchFocus(g *gocui.Gui, oldView, newView *gocui.View, returnin // we assume we'll never want to return focus to a popup panel i.e. // we should never stack popup panels if oldView != nil && !gui.isPopupPanel(oldView.Name()) && !returning { - gui.pushPreviousView(oldView.Name()) + gui.pushView(oldView.Name()) } gui.Log.Info("setting highlight to true for view " + newView.Name()) From f5c9e35a463a7cf2ab97e9a2526b6e496959b0af Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sun, 9 Oct 2022 16:48:35 -0700 Subject: [PATCH 17/42] improve subprocess approach --- pkg/app/app.go | 3 +- pkg/gui/containers_panel.go | 6 +-- pkg/gui/custom_commands.go | 3 +- pkg/gui/gui.go | 73 +++++++++++++------------------------ pkg/gui/project_panel.go | 3 +- pkg/gui/services_panel.go | 9 ++--- pkg/gui/subprocess.go | 68 ++++++++++++++++------------------ pkg/i18n/dutch.go | 1 - pkg/i18n/english.go | 2 - pkg/i18n/german.go | 1 - pkg/i18n/polish.go | 1 - pkg/i18n/turkish.go | 1 - 12 files changed, 65 insertions(+), 106 deletions(-) diff --git a/pkg/app/app.go b/pkg/app/app.go index a920f440..09a49789 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -56,8 +56,7 @@ func NewApp(config *config.AppConfig) (*App, error) { } func (app *App) Run() error { - err := app.Gui.RunWithSubprocesses() - return err + return app.Gui.Run() } func (app *App) Close() error { diff --git a/pkg/gui/containers_panel.go b/pkg/gui/containers_panel.go index 8978b99e..e4309e72 100644 --- a/pkg/gui/containers_panel.go +++ b/pkg/gui/containers_panel.go @@ -492,8 +492,7 @@ func (gui *Gui) handleContainerAttach(g *gocui.Gui, v *gocui.View) error { return gui.createErrorPanel(gui.g, err.Error()) } - gui.SubProcess = c - return gui.Errors.ErrSubProcess + return gui.runSubprocess(c) } func (gui *Gui) handlePruneContainers() error { @@ -537,8 +536,7 @@ func (gui *Gui) containerExecShell(container *commands.Container) error { resolvedCommand := utils.ApplyTemplate("docker exec -it {{ .Container.ID }} /bin/sh -c 'eval $(grep ^$(id -un): /etc/passwd | cut -d : -f 7-)'", commandObject) // attach and return the subprocess error cmd := gui.OSCommand.ExecutableFromString(resolvedCommand) - gui.SubProcess = cmd - return gui.Errors.ErrSubProcess + return gui.runSubprocess(cmd) } func (gui *Gui) handleContainersCustomCommand(g *gocui.Gui, v *gocui.View) error { diff --git a/pkg/gui/custom_commands.go b/pkg/gui/custom_commands.go index 1a23b719..da03eed9 100644 --- a/pkg/gui/custom_commands.go +++ b/pkg/gui/custom_commands.go @@ -53,8 +53,7 @@ func (gui *Gui) createCommandMenu(customCommands []config.CustomCommand, command // if we have a command for attaching, we attach and return the subprocess error if option.customCommand.Attach { cmd := gui.OSCommand.ExecutableFromString(option.command) - gui.SubProcess = cmd - return gui.Errors.ErrSubProcess + return gui.runSubprocess(cmd) } return gui.WithWaitingStatus(waitingStatus, func() error { diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 17710987..c664edb0 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -2,20 +2,14 @@ package gui import ( "context" - "os/exec" "strings" "sync" "time" "github.com/docker/docker/api/types" - // "io" - // "io/ioutil" - "github.com/go-errors/errors" - // "strings" - throttle "github.com/boz/go-throttle" "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazydocker/pkg/commands" @@ -31,7 +25,6 @@ var OverlappingEdges = false // SentinelErrors are the errors that have special meaning and need to be checked // by calling functions. The less of these, the better type SentinelErrors struct { - ErrSubProcess error ErrNoContainers error ErrNoImages error ErrNoVolumes error @@ -49,7 +42,6 @@ type SentinelErrors struct { // localising things in the code. func (gui *Gui) GenerateSentinelErrors() { gui.Errors = SentinelErrors{ - ErrSubProcess: errors.New(gui.Tr.RunningSubprocess), ErrNoContainers: errors.New(gui.Tr.NoContainers), ErrNoImages: errors.New(gui.Tr.NoImages), ErrNoVolumes: errors.New(gui.Tr.NoVolumes), @@ -62,7 +54,6 @@ type Gui struct { Log *logrus.Entry DockerCommand *commands.DockerCommand OSCommand *commands.OSCommand - SubProcess *exec.Cmd State guiState Config *config.AppConfig Tr *i18n.TranslationSet @@ -73,8 +64,20 @@ type Gui struct { ErrorChan chan error CyclableViews []string Views Views - + // returns true if our views have been created and assigned to gui.Views. + // Views are setup only once, upon application start. ViewsSetup bool + + // if we've suspended the gui (e.g. because we've switched to a subprocess) + // we typically want to pause some things that are running like background + // file refreshes + PauseBackgroundThreads bool + + Mutexes +} + +type Mutexes struct { + SubprocessMutex sync.Mutex } type servicePanelState struct { @@ -130,11 +133,6 @@ type guiState struct { SubProcessOutput string Stats map[string]commands.ContainerStats - // SessionIndex tells us how many times we've come back from a subprocess. - // We increment it each time we switch to a new subprocess - // Every time we go to a subprocess we need to close a few goroutines so this index is used for that purpose - SessionIndex int - ScreenMode WindowMaximisation } @@ -165,8 +163,7 @@ func NewGui(log *logrus.Entry, dockerCommand *commands.DockerCommand, oSCommand }, Project: &projectState{ContextIndex: 0}, }, - SessionIndex: 0, - ViewStack: []string{}, + ViewStack: []string{}, } cyclableViews := []string{"project", "containers", "images", "volumes"} @@ -193,13 +190,6 @@ func NewGui(log *logrus.Entry, dockerCommand *commands.DockerCommand, oSCommand return gui, nil } -func max(a, b int) int { - if a > b { - return a - } - return b -} - func (gui *Gui) renderGlobalOptions() error { return gui.renderOptionsMap(map[string]string{ "PgUp/PgDn": gui.Tr.Scroll, @@ -211,15 +201,15 @@ func (gui *Gui) renderGlobalOptions() error { } func (gui *Gui) goEvery(interval time.Duration, function func() error) { - currentSessionIndex := gui.State.SessionIndex _ = function() // time.Tick doesn't run immediately so we'll do that here // TODO: maybe change go func() { ticker := time.NewTicker(interval) defer ticker.Stop() for range ticker.C { - if gui.State.SessionIndex > currentSessionIndex { + if gui.PauseBackgroundThreads { return } + _ = function() } }() @@ -289,6 +279,9 @@ func (gui *Gui) Run() error { } err = g.MainLoop() + if err == gocui.ErrQuit { + return nil + } return err } @@ -411,8 +404,12 @@ func (gui *Gui) handleDonate(g *gocui.Gui, v *gocui.View) error { } func (gui *Gui) editFile(filename string) error { - _, err := gui.runSyncOrAsyncCommand(gui.OSCommand.EditFile(filename)) - return err + cmd, err := gui.OSCommand.EditFile(filename) + if err != nil { + return gui.createErrorPanel(gui.g, err.Error()) + } + + return gui.runSubprocess(cmd) } func (gui *Gui) openFile(filename string) error { @@ -422,28 +419,10 @@ func (gui *Gui) openFile(filename string) error { return nil } -// runSyncOrAsyncCommand takes the output of a command that may have returned -// either no error, an error, or a subprocess to execute, and if a subprocess -// needs to be set on the gui object, it does so, and then returns the error -// the bool returned tells us whether the calling code should continue -func (gui *Gui) runSyncOrAsyncCommand(sub *exec.Cmd, err error) (bool, error) { - if err != nil { - if err != gui.Errors.ErrSubProcess { - return false, gui.createErrorPanel(gui.g, err.Error()) - } - } - if sub != nil { - gui.SubProcess = sub - return false, gui.Errors.ErrSubProcess - } - return true, nil -} - func (gui *Gui) handleCustomCommand(g *gocui.Gui, v *gocui.View) error { return gui.createPromptPanel(g, v, gui.Tr.CustomCommandTitle, func(g *gocui.Gui, v *gocui.View) error { command := gui.trimmedContent(v) - gui.SubProcess = gui.OSCommand.RunCustomCommand(command) - return gui.Errors.ErrSubProcess + return gui.runSubprocess(gui.OSCommand.RunCustomCommand(command)) }) } diff --git a/pkg/gui/project_panel.go b/pkg/gui/project_panel.go index 743c6825..3dd14175 100644 --- a/pkg/gui/project_panel.go +++ b/pkg/gui/project_panel.go @@ -224,6 +224,5 @@ func (gui *Gui) handleViewAllLogs(g *gocui.Gui, v *gocui.View) error { return gui.createErrorPanel(gui.g, err.Error()) } - gui.SubProcess = c - return gui.Errors.ErrSubProcess + return gui.runSubprocess(c) } diff --git a/pkg/gui/services_panel.go b/pkg/gui/services_panel.go index d290911e..09a4dafb 100644 --- a/pkg/gui/services_panel.go +++ b/pkg/gui/services_panel.go @@ -293,8 +293,7 @@ func (gui *Gui) handleServiceAttach(g *gocui.Gui, v *gocui.View) error { return gui.createErrorPanel(gui.g, err.Error()) } - gui.SubProcess = c - return gui.Errors.ErrSubProcess + return gui.runSubprocess(c) } func (gui *Gui) handleServiceRenderLogsToMain(g *gocui.Gui, v *gocui.View) error { @@ -308,8 +307,7 @@ func (gui *Gui) handleServiceRenderLogsToMain(g *gocui.Gui, v *gocui.View) error return gui.createErrorPanel(gui.g, err.Error()) } - gui.SubProcess = c - return gui.Errors.ErrSubProcess + return gui.runSubprocess(c) } func (gui *Gui) handleServiceRestartMenu(g *gocui.Gui, v *gocui.View) error { @@ -366,8 +364,7 @@ func (gui *Gui) handleServiceRestartMenu(g *gocui.Gui, v *gocui.View) error { gui.DockerCommand.NewCommandObject(commands.CommandObject{Service: service}), ), f: func() error { - gui.SubProcess = gui.OSCommand.RunCustomCommand(rebuildCommand) - return gui.Errors.ErrSubProcess + return gui.runSubprocess(gui.OSCommand.RunCustomCommand(rebuildCommand)) }, }, { diff --git a/pkg/gui/subprocess.go b/pkg/gui/subprocess.go index 059f05e1..6d114bc8 100644 --- a/pkg/gui/subprocess.go +++ b/pkg/gui/subprocess.go @@ -4,48 +4,43 @@ import ( "fmt" "io/ioutil" "os" + "os/exec" "os/signal" "strings" "github.com/fatih/color" - "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazydocker/pkg/utils" ) -// RunWithSubprocesses loops, instantiating a new gocui.Gui with each iteration -// if the error returned from a run is a ErrSubProcess, it runs the subprocess -// otherwise it handles the error, possibly by quitting the application -func (gui *Gui) RunWithSubprocesses() error { - for { - if err := gui.Run(); err != nil { - if err == gocui.ErrQuit { - break - } else if err == gui.Errors.ErrSubProcess { - // preparing the state for when we return - gui.pushView(gui.currentViewName()) - // giving goEvery goroutines time to finish - gui.State.SessionIndex++ +func (gui *Gui) runSubprocess(cmd *exec.Cmd) error { + gui.Mutexes.SubprocessMutex.Lock() + defer gui.Mutexes.SubprocessMutex.Unlock() - if err := gui.runCommand(); err != nil { - return err - } - - // pop here so we don't stack up view names - gui.popView() - // ensuring we render e.g. the logs of the currently selected item upon return - gui.State.Panels.Main.ObjectKey = "" - } else { - return err - } - } + if err := gui.g.Suspend(); err != nil { + return gui.createErrorPanel(gui.g, err.Error()) } + + gui.PauseBackgroundThreads = true + + cmdErr := gui.runSubprocess(cmd) + + if err := gui.g.Resume(); err != nil { + return gui.createErrorPanel(gui.g, err.Error()) + } + + gui.PauseBackgroundThreads = false + + if cmdErr != nil { + return gui.createErrorPanel(gui.g, cmdErr.Error()) + } + return nil } -func (gui *Gui) runCommand() error { - gui.SubProcess.Stdout = os.Stdout - gui.SubProcess.Stderr = os.Stdout - gui.SubProcess.Stdin = os.Stdin +func (gui *Gui) runCommand(cmd *exec.Cmd) error { + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stdout + cmd.Stdin = os.Stdin stop := make(chan os.Signal, 1) defer signal.Stop(stop) @@ -54,23 +49,22 @@ func (gui *Gui) runCommand() error { signal.Notify(stop, os.Interrupt) <-stop - if err := gui.OSCommand.Kill(gui.SubProcess); err != nil { + if err := gui.OSCommand.Kill(cmd); err != nil { gui.Log.Error(err) } }() - fmt.Fprintf(os.Stdout, "\n%s\n\n", utils.ColoredString("+ "+strings.Join(gui.SubProcess.Args, " "), color.FgBlue)) + fmt.Fprintf(os.Stdout, "\n%s\n\n", utils.ColoredString("+ "+strings.Join(cmd.Args, " "), color.FgBlue)) - if err := gui.SubProcess.Run(); err != nil { + if err := cmd.Run(); err != nil { // not handling the error explicitly because usually we're going to see it // in the output anyway gui.Log.Error(err) } - gui.SubProcess.Stdin = nil - gui.SubProcess.Stdout = ioutil.Discard - gui.SubProcess.Stderr = ioutil.Discard - gui.SubProcess = nil + cmd.Stdin = nil + cmd.Stdout = ioutil.Discard + cmd.Stderr = ioutil.Discard gui.promptToReturn() diff --git a/pkg/i18n/dutch.go b/pkg/i18n/dutch.go index 90a75f31..1532eeea 100644 --- a/pkg/i18n/dutch.go +++ b/pkg/i18n/dutch.go @@ -8,7 +8,6 @@ func dutchSet() TranslationSet { StoppingStatus: "stoppen", RunningCustomCommandStatus: "Aangepast commando draaien", - RunningSubprocess: "subprocess draaien", NoViewMachingNewLineFocusedSwitchStatement: "No view matching newLineFocused switch statement", ErrorOccurred: "Er is iets fout gegaan! Zou je hier een issue aan willen maken: https://github.com/jesseduffield/lazydocker/issues", diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 62d9a1c4..9f225c4d 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -13,7 +13,6 @@ type TranslationSet struct { Scroll string Close string ErrorTitle string - RunningSubprocess string NoViewMachingNewLineFocusedSwitchStatement string OpenConfig string EditConfig string @@ -121,7 +120,6 @@ func englishSet() TranslationSet { RunningCustomCommandStatus: "running custom command", RunningBulkCommandStatus: "running bulk command", - RunningSubprocess: "running subprocess", NoViewMachingNewLineFocusedSwitchStatement: "No view matching newLineFocused switch statement", ErrorOccurred: "An error occurred! Please create an issue at https://github.com/jesseduffield/lazydocker/issues", diff --git a/pkg/i18n/german.go b/pkg/i18n/german.go index aba4f73c..4a205aa2 100644 --- a/pkg/i18n/german.go +++ b/pkg/i18n/german.go @@ -8,7 +8,6 @@ func germanSet() TranslationSet { StoppingStatus: "anhalten", RunningCustomCommandStatus: "führt benutzerdefinierten Befehl aus", - RunningSubprocess: "Unterprozess ausführen", NoViewMachingNewLineFocusedSwitchStatement: "No view matching newLineFocused switch statement", ErrorOccurred: "Es ist ein Fehler aufgetreten! Bitte erstelle ein Issue hier: https://github.com/jesseduffield/lazydocker/issues", diff --git a/pkg/i18n/polish.go b/pkg/i18n/polish.go index 2525d639..2cf12dd0 100644 --- a/pkg/i18n/polish.go +++ b/pkg/i18n/polish.go @@ -8,7 +8,6 @@ func polishSet() TranslationSet { StoppingStatus: "zatrzymywanie", RunningCustomCommandStatus: "uruchamianie własnej komendty", - RunningSubprocess: "uruchamianie podprocesu", NoViewMachingNewLineFocusedSwitchStatement: "Żaden widok nie odpowiada instrukcji przełączenia newLineFocused", ErrorOccurred: "Wystąpił błąd! Proszę go zgłosić na https://github.com/jesseduffield/lazydocker/issues", diff --git a/pkg/i18n/turkish.go b/pkg/i18n/turkish.go index 9b51af22..4a378b7a 100644 --- a/pkg/i18n/turkish.go +++ b/pkg/i18n/turkish.go @@ -8,7 +8,6 @@ func turkishSet() TranslationSet { StoppingStatus: "durduruluyor", RunningCustomCommandStatus: "özel komut çalıştır", - RunningSubprocess: "İkincil işlem yürütülüyor", NoViewMachingNewLineFocusedSwitchStatement: "NewLineFocused anahtar deyimi ile eşleşen görünüm yok", ErrorOccurred: "Bir hata oluştu! Lütfen https://github.com/jesseduffield/lazydocker/issues adresinden bir hataya ilişkin konu oluşturun", From 9dac4864f28d5d3735a4e91b50674eb5455d01e9 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sun, 9 Oct 2022 17:13:43 -0700 Subject: [PATCH 18/42] fix focus issues --- pkg/gui/arrangement.go | 31 +++-------- pkg/gui/confirmation_panel.go | 6 +-- pkg/gui/gui.go | 1 + pkg/gui/layout.go | 11 ++-- pkg/gui/main_panel.go | 4 +- pkg/gui/menu_panel.go | 3 +- pkg/gui/subprocess.go | 2 +- pkg/gui/view_helpers.go | 98 +++++++++++++++-------------------- 8 files changed, 63 insertions(+), 93 deletions(-) diff --git a/pkg/gui/arrangement.go b/pkg/gui/arrangement.go index b24c82af..6b0250e5 100644 --- a/pkg/gui/arrangement.go +++ b/pkg/gui/arrangement.go @@ -204,30 +204,15 @@ func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box { } } -// TODO: reintroduce -// func (gui *Gui) currentSideWindowName() string { -// // there is always one and only one cyclable context in the context stack. We'll look from top to bottom -// gui.State.ContextManager.RLock() -// defer gui.State.ContextManager.RUnlock() - -// for idx := range gui.State.ContextManager.ContextStack { -// reversedIdx := len(gui.State.ContextManager.ContextStack) - 1 - idx -// context := gui.State.ContextManager.ContextStack[reversedIdx] - -// if context.GetKind() == types.SIDE_CONTEXT { -// return context.GetWindowName() -// } -// } - -// return "files" // default -// } - -// TODO: do this better. func (gui *Gui) currentSideWindowName() string { - windowName := gui.currentWindow() - if !lo.Contains(gui.sideViewNames(), windowName) { - return gui.peekPreviousView() + // we expect that there is a side window somewhere in the view stack, so we will search from top to bottom + for idx := range gui.State.ViewStack { + reversedIdx := len(gui.State.ViewStack) - 1 - idx + viewName := gui.State.ViewStack[reversedIdx] + if lo.Contains(gui.sideViewNames(), viewName) { + return viewName + } } - return windowName + return gui.initiallyFocusedViewName() } diff --git a/pkg/gui/confirmation_panel.go b/pkg/gui/confirmation_panel.go index 4c3b761a..85f3fe85 100644 --- a/pkg/gui/confirmation_panel.go +++ b/pkg/gui/confirmation_panel.go @@ -86,7 +86,7 @@ func (gui *Gui) prepareConfirmationPanel(currentView *gocui.View, title, prompt confirmationView.Title = title confirmationView.Visible = true gui.g.Update(func(g *gocui.Gui) error { - return gui.switchFocus(gui.g, currentView, confirmationView, false) + return gui.switchFocus(confirmationView) }) return nil } @@ -96,8 +96,8 @@ func (gui *Gui) onNewPopupPanel() { gui.Views.Confirmation.Visible = false } -// 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 -// unparam complains that handleClose is alwans nil but one day it won't be nil. +// 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. +// The golangcilint unparam linter complains that handleClose is alwans nil but one day it won't be nil. // nolint:unparam func (gui *Gui) createConfirmationPanel(g *gocui.Gui, currentView *gocui.View, title, prompt string, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error { return gui.createPopupPanel(g, currentView, title, prompt, false, handleConfirm, handleClose) diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index c664edb0..1b2199a7 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -78,6 +78,7 @@ type Gui struct { type Mutexes struct { SubprocessMutex sync.Mutex + ViewStackMutex sync.Mutex } type servicePanelState struct { diff --git a/pkg/gui/layout.go b/pkg/gui/layout.go index 22f5fd89..11e86758 100644 --- a/pkg/gui/layout.go +++ b/pkg/gui/layout.go @@ -117,16 +117,13 @@ func (gui *Gui) layout(g *gocui.Gui) error { } if gui.g.CurrentView() == nil { - v, err := gui.g.View(gui.peekPreviousView()) + viewName := gui.initiallyFocusedViewName() + view, err := gui.g.View(viewName) if err != nil { - viewName := gui.initiallyFocusedViewName() - v, err = gui.g.View(viewName) - if err != nil { - return err - } + return err } - if err := gui.switchFocus(gui.g, nil, v, false); err != nil { + if err := gui.switchFocus(view); err != nil { return err } } diff --git a/pkg/gui/main_panel.go b/pkg/gui/main_panel.go index 6b59ef73..c8f13721 100644 --- a/pkg/gui/main_panel.go +++ b/pkg/gui/main_panel.go @@ -101,7 +101,7 @@ func (gui *Gui) handleEnterMain(g *gocui.Gui, v *gocui.View) error { mainView := gui.getMainView() mainView.ParentView = v - return gui.switchFocus(gui.g, v, mainView, false) + return gui.switchFocus(mainView) } func (gui *Gui) handleExitMain(g *gocui.Gui, v *gocui.View) error { @@ -122,5 +122,5 @@ func (gui *Gui) handleMainClick(g *gocui.Gui, v *gocui.View) error { v.ParentView = currentView } - return gui.switchFocus(gui.g, currentView, v, false) + return gui.switchFocus(v) } diff --git a/pkg/gui/menu_panel.go b/pkg/gui/menu_panel.go index adb35818..f6e5e74b 100644 --- a/pkg/gui/menu_panel.go +++ b/pkg/gui/menu_panel.go @@ -100,8 +100,7 @@ func (gui *Gui) createMenu(title string, items interface{}, itemCount int, handl gui.g.Update(func(g *gocui.Gui) error { menuView.Visible = true - currentView := gui.g.CurrentView() - return gui.switchFocus(gui.g, currentView, menuView, false) + return gui.switchFocus(menuView) }) return nil } diff --git a/pkg/gui/subprocess.go b/pkg/gui/subprocess.go index 6d114bc8..54af269d 100644 --- a/pkg/gui/subprocess.go +++ b/pkg/gui/subprocess.go @@ -22,7 +22,7 @@ func (gui *Gui) runSubprocess(cmd *exec.Cmd) error { gui.PauseBackgroundThreads = true - cmdErr := gui.runSubprocess(cmd) + cmdErr := gui.runCommand(cmd) if err := gui.g.Resume(); err != nil { return gui.createErrorPanel(gui.g, err.Error()) diff --git a/pkg/gui/view_helpers.go b/pkg/gui/view_helpers.go index a585d807..2ab39f19 100644 --- a/pkg/gui/view_helpers.go +++ b/pkg/gui/view_helpers.go @@ -33,8 +33,7 @@ func (gui *Gui) nextView(g *gocui.Gui, v *gocui.View) error { panic(err) } gui.resetMainView() - gui.popView() - return gui.switchFocus(g, v, focusedView, false) + return gui.switchFocus(focusedView) } func (gui *Gui) previousView(g *gocui.Gui, v *gocui.View) error { @@ -59,8 +58,7 @@ func (gui *Gui) previousView(g *gocui.Gui, v *gocui.View) error { panic(err) } gui.resetMainView() - gui.popView() - return gui.switchFocus(g, v, focusedView, false) + return gui.switchFocus(focusedView) } func (gui *Gui) resetMainView() { @@ -96,24 +94,48 @@ func (gui *Gui) newLineFocused(v *gocui.View) error { } } -func (gui *Gui) popView() string { - if len(gui.State.ViewStack) > 0 { - value := gui.State.ViewStack[len(gui.State.ViewStack)-1] - gui.State.ViewStack = gui.State.ViewStack[:len(gui.State.ViewStack)-1] - return value - } +// TODO: move some of this logic into our onFocusLost and onFocus hooks +func (gui *Gui) switchFocus(newView *gocui.View) error { + gui.Mutexes.ViewStackMutex.Lock() + defer gui.Mutexes.ViewStackMutex.Unlock() - return "" + return gui.switchFocusAux(newView) } -func (gui *Gui) peekPreviousView() string { - if len(gui.State.ViewStack) > 0 { - return gui.State.ViewStack[len(gui.State.ViewStack)-1] +func (gui *Gui) switchFocusAux(newView *gocui.View) error { + gui.pushView(newView.Name()) + gui.Log.Info("setting highlight to true for view " + newView.Name()) + gui.Log.Info("new focused view is " + newView.Name()) + if _, err := gui.g.SetCurrentView(newView.Name()); err != nil { + return err } - return "" + gui.g.Cursor = newView.Editable + + if err := gui.renderPanelOptions(); err != nil { + return err + } + + return gui.newLineFocused(newView) } +func (gui *Gui) returnFocus(g *gocui.Gui, v *gocui.View) error { + gui.Mutexes.ViewStackMutex.Lock() + defer gui.Mutexes.ViewStackMutex.Unlock() + + if len(gui.State.ViewStack) <= 1 { + return nil + } + + previousViewName := gui.State.ViewStack[len(gui.State.ViewStack)-2] + previousView, err := g.View(previousViewName) + if err != nil { + return err + } + return gui.switchFocusAux(previousView) +} + +// Not to be called directly. Use `switchFocus` instead func (gui *Gui) pushView(name string) { // No matter what view we're pushing, we first remove all popup panels from the stack gui.State.ViewStack = lo.Filter(gui.State.ViewStack, func(viewName string, _ int) bool { @@ -122,51 +144,17 @@ func (gui *Gui) pushView(name string) { // If we're pushing a side panel, we remove all other panels if lo.Contains(gui.sideViewNames(), name) { - gui.State.ViewStack = lo.Filter(gui.State.ViewStack, func(viewName string, _ int) bool { - return viewName != "main" - }) + gui.State.ViewStack = []string{} } + // If we're pushing a panel that's already in the stack, we remove it + gui.State.ViewStack = lo.Filter(gui.State.ViewStack, func(viewName string, _ int) bool { + return viewName != name + }) + gui.State.ViewStack = append(gui.State.ViewStack, name) } -func (gui *Gui) returnFocus(g *gocui.Gui, v *gocui.View) error { - previousViewName := gui.popView() - previousView, err := g.View(previousViewName) - if err != nil { - // always fall back to services view if there's no 'previous' view stored - previousView, err = g.View(gui.initiallyFocusedViewName()) - if err != nil { - gui.Log.Error(err) - } - } - return gui.switchFocus(g, v, previousView, true) -} - -// pass in oldView = nil if you don't want to be able to return to your old view -// TODO: move some of this logic into our onFocusLost and onFocus hooks -func (gui *Gui) switchFocus(g *gocui.Gui, oldView, newView *gocui.View, returning bool) error { - // we assume we'll never want to return focus to a popup panel i.e. - // we should never stack popup panels - if oldView != nil && !gui.isPopupPanel(oldView.Name()) && !returning { - gui.pushView(oldView.Name()) - } - - gui.Log.Info("setting highlight to true for view " + newView.Name()) - gui.Log.Info("new focused view is " + newView.Name()) - if _, err := g.SetCurrentView(newView.Name()); err != nil { - return err - } - - g.Cursor = newView.Editable - - if err := gui.renderPanelOptions(); err != nil { - return err - } - - return gui.newLineFocused(newView) -} - // if the cursor down past the last item, move it to the last line // nolint:unparam func (gui *Gui) focusPoint(selectedX int, selectedY int, lineCount int, v *gocui.View) { From a2f90b6ea9285a83eca3ea81e948f63fbb4fc2f6 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sun, 9 Oct 2022 17:48:42 -0700 Subject: [PATCH 19/42] prevent issue where opening popup in maximised main view shows popup in front of a side view --- pkg/gui/arrangement.go | 2 +- pkg/gui/confirmation_panel.go | 18 +++++++++--------- pkg/gui/containers_panel.go | 10 +++++----- pkg/gui/gui.go | 2 +- pkg/gui/images_panel.go | 4 ++-- pkg/gui/main_panel.go | 2 +- pkg/gui/menu_panel.go | 12 +++++++++--- pkg/gui/options_menu_panel.go | 5 +---- pkg/gui/services_panel.go | 2 +- pkg/gui/view_helpers.go | 19 +++++++++++++++++-- pkg/gui/views.go | 4 ++++ pkg/gui/volumes_panel.go | 2 +- pkg/gui/window.go | 5 +++++ 13 files changed, 57 insertions(+), 30 deletions(-) diff --git a/pkg/gui/arrangement.go b/pkg/gui/arrangement.go index 6b0250e5..df717341 100644 --- a/pkg/gui/arrangement.go +++ b/pkg/gui/arrangement.go @@ -75,7 +75,7 @@ func MergeMaps[K comparable, V any](maps ...map[K]V) map[K]V { } func (gui *Gui) getMidSectionWeights() (int, int) { - currentWindow := gui.currentWindow() + currentWindow := gui.currentStaticWindowName() // we originally specified this as a ratio i.e. .20 would correspond to a weight of 1 against 4 sidePanelWidthRatio := gui.Config.UserConfig.Gui.SidePanelWidth diff --git a/pkg/gui/confirmation_panel.go b/pkg/gui/confirmation_panel.go index 85f3fe85..c515fd54 100644 --- a/pkg/gui/confirmation_panel.go +++ b/pkg/gui/confirmation_panel.go @@ -30,7 +30,7 @@ func (gui *Gui) wrappedConfirmationFunction(function func(*gocui.Gui, *gocui.Vie } func (gui *Gui) closeConfirmationPrompt() error { - if err := gui.returnFocus(gui.g, gui.Views.Confirmation); err != nil { + if err := gui.returnFocus(); err != nil { return err } gui.g.DeleteViewKeybindings("confirmation") @@ -64,7 +64,7 @@ func (gui *Gui) getConfirmationPanelDimensions(g *gocui.Gui, wrap bool, prompt s func (gui *Gui) createPromptPanel(g *gocui.Gui, currentView *gocui.View, title string, handleConfirm func(*gocui.Gui, *gocui.View) error) error { gui.onNewPopupPanel() - err := gui.prepareConfirmationPanel(currentView, title, "", false) + err := gui.prepareConfirmationPanel(title, "", false) if err != nil { return err } @@ -72,7 +72,7 @@ func (gui *Gui) createPromptPanel(g *gocui.Gui, currentView *gocui.View, title s return gui.setKeyBindings(g, handleConfirm, nil) } -func (gui *Gui) prepareConfirmationPanel(currentView *gocui.View, title, prompt string, hasLoader bool) error { +func (gui *Gui) prepareConfirmationPanel(title, prompt string, hasLoader bool) error { x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(gui.g, true, prompt) confirmationView := gui.Views.Confirmation _, err := gui.g.SetView("confirmation", x0, y0, x1, y1, 0) @@ -99,19 +99,19 @@ func (gui *Gui) onNewPopupPanel() { // 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. // The golangcilint unparam linter complains that handleClose is alwans nil but one day it won't be nil. // nolint:unparam -func (gui *Gui) createConfirmationPanel(g *gocui.Gui, currentView *gocui.View, title, prompt string, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error { - return gui.createPopupPanel(g, currentView, title, prompt, false, handleConfirm, handleClose) +func (gui *Gui) createConfirmationPanel(title, prompt string, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error { + return gui.createPopupPanel(title, prompt, false, handleConfirm, handleClose) } -func (gui *Gui) createPopupPanel(g *gocui.Gui, currentView *gocui.View, title, prompt string, hasLoader bool, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error { +func (gui *Gui) createPopupPanel(title, prompt string, hasLoader bool, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error { gui.onNewPopupPanel() - g.Update(func(g *gocui.Gui) error { + gui.g.Update(func(g *gocui.Gui) error { if gui.currentViewName() == "confirmation" { if err := gui.closeConfirmationPrompt(); err != nil { gui.Log.Error(err.Error()) } } - err := gui.prepareConfirmationPanel(currentView, title, prompt, hasLoader) + err := gui.prepareConfirmationPanel(title, prompt, hasLoader) if err != nil { return err } @@ -146,7 +146,7 @@ func (gui *Gui) setKeyBindings(g *gocui.Gui, handleConfirm, handleClose func(*go func (gui *Gui) createErrorPanel(g *gocui.Gui, message string) error { colorFunction := color.New(color.FgRed).SprintFunc() coloredMessage := colorFunction(strings.TrimSpace(message)) - return gui.createConfirmationPanel(gui.g, g.CurrentView(), gui.Tr.ErrorTitle, coloredMessage, nil, nil) + return gui.createConfirmationPanel(gui.Tr.ErrorTitle, coloredMessage, nil, nil) } func (gui *Gui) renderConfirmationOptions() error { diff --git a/pkg/gui/containers_panel.go b/pkg/gui/containers_panel.go index e4309e72..3dc9bd2f 100644 --- a/pkg/gui/containers_panel.go +++ b/pkg/gui/containers_panel.go @@ -408,7 +408,7 @@ func (gui *Gui) handleContainersRemoveMenu(g *gocui.Gui, v *gocui.View) error { return gui.WithWaitingStatus(gui.Tr.RemovingStatus, func() error { if err := container.Remove(configOptions); err != nil { if commands.HasErrorCode(err, commands.MustStopContainer) { - return gui.createConfirmationPanel(gui.g, v, gui.Tr.Confirm, gui.Tr.MustForceToRemoveContainer, func(g *gocui.Gui, v *gocui.View) error { + return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.MustForceToRemoveContainer, func(g *gocui.Gui, v *gocui.View) error { return gui.WithWaitingStatus(gui.Tr.RemovingStatus, func() error { configOptions.Force = true return container.Remove(configOptions) @@ -455,7 +455,7 @@ func (gui *Gui) handleContainerStop(g *gocui.Gui, v *gocui.View) error { return nil } - return gui.createConfirmationPanel(gui.g, v, gui.Tr.Confirm, gui.Tr.StopContainer, func(g *gocui.Gui, v *gocui.View) error { + return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.StopContainer, func(g *gocui.Gui, v *gocui.View) error { return gui.WithWaitingStatus(gui.Tr.StoppingStatus, func() error { if err := container.Stop(); err != nil { return gui.createErrorPanel(gui.g, err.Error()) @@ -496,7 +496,7 @@ func (gui *Gui) handleContainerAttach(g *gocui.Gui, v *gocui.View) error { } func (gui *Gui) handlePruneContainers() error { - return gui.createConfirmationPanel(gui.g, gui.getContainersView(), gui.Tr.Confirm, gui.Tr.ConfirmPruneContainers, func(g *gocui.Gui, v *gocui.View) error { + return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.ConfirmPruneContainers, func(g *gocui.Gui, v *gocui.View) error { return gui.WithWaitingStatus(gui.Tr.PruningStatus, func() error { err := gui.DockerCommand.PruneContainers() if err != nil { @@ -555,7 +555,7 @@ func (gui *Gui) handleContainersCustomCommand(g *gocui.Gui, v *gocui.View) error } func (gui *Gui) handleStopContainers() error { - return gui.createConfirmationPanel(gui.g, gui.getContainersView(), gui.Tr.Confirm, gui.Tr.ConfirmStopContainers, func(g *gocui.Gui, v *gocui.View) error { + return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.ConfirmStopContainers, func(g *gocui.Gui, v *gocui.View) error { return gui.WithWaitingStatus(gui.Tr.StoppingStatus, func() error { for _, container := range gui.DockerCommand.Containers { _ = container.Stop() @@ -567,7 +567,7 @@ func (gui *Gui) handleStopContainers() error { } func (gui *Gui) handleRemoveContainers() error { - return gui.createConfirmationPanel(gui.g, gui.getContainersView(), gui.Tr.Confirm, gui.Tr.ConfirmRemoveContainers, func(g *gocui.Gui, v *gocui.View) error { + return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.ConfirmRemoveContainers, func(g *gocui.Gui, v *gocui.View) error { return gui.WithWaitingStatus(gui.Tr.RemovingStatus, func() error { for _, container := range gui.DockerCommand.Containers { _ = container.Remove(types.ContainerRemoveOptions{Force: true}) diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 1b2199a7..94aa4c0d 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -385,7 +385,7 @@ func (gui *Gui) reRenderMain() error { func (gui *Gui) quit(g *gocui.Gui, v *gocui.View) error { if gui.Config.UserConfig.ConfirmOnQuit { - return gui.createConfirmationPanel(g, v, "", gui.Tr.ConfirmQuit, func(g *gocui.Gui, v *gocui.View) error { + return gui.createConfirmationPanel("", gui.Tr.ConfirmQuit, func(g *gocui.Gui, v *gocui.View) error { return gocui.ErrQuit }, nil) } diff --git a/pkg/gui/images_panel.go b/pkg/gui/images_panel.go index da69c93f..2f513ab5 100644 --- a/pkg/gui/images_panel.go +++ b/pkg/gui/images_panel.go @@ -116,7 +116,7 @@ func (gui *Gui) refreshImages() error { gui.g.Update(func(g *gocui.Gui) error { ImagesView.Clear() - isFocused := gui.g.CurrentView().Name() == "Images" + isFocused := gui.g.CurrentView() == gui.Views.Images list, err := utils.RenderList(gui.DockerCommand.Images, utils.IsFocused(isFocused)) if err != nil { return err @@ -260,7 +260,7 @@ func (gui *Gui) handleImagesRemoveMenu(g *gocui.Gui, v *gocui.View) error { } func (gui *Gui) handlePruneImages() error { - return gui.createConfirmationPanel(gui.g, gui.getImagesView(), gui.Tr.Confirm, gui.Tr.ConfirmPruneImages, func(g *gocui.Gui, v *gocui.View) error { + return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.ConfirmPruneImages, func(g *gocui.Gui, v *gocui.View) error { return gui.WithWaitingStatus(gui.Tr.PruningStatus, func() error { err := gui.DockerCommand.PruneImages() if err != nil { diff --git a/pkg/gui/main_panel.go b/pkg/gui/main_panel.go index c8f13721..e8675df4 100644 --- a/pkg/gui/main_panel.go +++ b/pkg/gui/main_panel.go @@ -106,7 +106,7 @@ func (gui *Gui) handleEnterMain(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleExitMain(g *gocui.Gui, v *gocui.View) error { v.ParentView = nil - return gui.returnFocus(gui.g, v) + return gui.returnFocus() } func (gui *Gui) handleMainClick(g *gocui.Gui, v *gocui.View) error { diff --git a/pkg/gui/menu_panel.go b/pkg/gui/menu_panel.go index f6e5e74b..66e0d316 100644 --- a/pkg/gui/menu_panel.go +++ b/pkg/gui/menu_panel.go @@ -58,7 +58,7 @@ func (gui *Gui) handleMenuClose(g *gocui.Gui, v *gocui.View) error { } } gui.Views.Menu.Visible = false - return gui.returnFocus(g, v) + return gui.returnFocus() } func (gui *Gui) createMenu(title string, items interface{}, itemCount int, handlePress func(int) error) error { @@ -80,12 +80,18 @@ func (gui *Gui) createMenu(title string, items interface{}, itemCount int, handl wrappedHandlePress := func(g *gocui.Gui, v *gocui.View) error { selectedLine := gui.State.Panels.Menu.SelectedLine + + menuView.Visible = false + err := gui.returnFocus() + if err != nil { + return err + } + if err := handlePress(selectedLine); err != nil { return err } - menuView.Visible = false - return gui.returnFocus(gui.g, menuView) + return nil } gui.State.Panels.Menu.OnPress = wrappedHandlePress diff --git a/pkg/gui/options_menu_panel.go b/pkg/gui/options_menu_panel.go index 24dd9681..c5a4c2b6 100644 --- a/pkg/gui/options_menu_panel.go +++ b/pkg/gui/options_menu_panel.go @@ -64,10 +64,7 @@ func (gui *Gui) handleCreateOptionsMenu(g *gocui.Gui, v *gocui.View) error { if index >= len(bindings) { return errors.New("Index is greater than size of bindings") } - err := gui.handleMenuClose(g, v) - if err != nil { - return err - } + return bindings[index].Handler(g, v) } diff --git a/pkg/gui/services_panel.go b/pkg/gui/services_panel.go index 09a4dafb..16accf28 100644 --- a/pkg/gui/services_panel.go +++ b/pkg/gui/services_panel.go @@ -237,7 +237,7 @@ func (gui *Gui) handleServiceStop(g *gocui.Gui, v *gocui.View) error { return nil } - return gui.createConfirmationPanel(gui.g, v, gui.Tr.Confirm, gui.Tr.StopService, func(g *gocui.Gui, v *gocui.View) error { + return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.StopService, func(g *gocui.Gui, v *gocui.View) error { return gui.WithWaitingStatus(gui.Tr.StoppingStatus, func() error { if err := service.Stop(); err != nil { return gui.createErrorPanel(gui.g, err.Error()) diff --git a/pkg/gui/view_helpers.go b/pkg/gui/view_helpers.go index 2ab39f19..740ccbc1 100644 --- a/pkg/gui/view_helpers.go +++ b/pkg/gui/view_helpers.go @@ -119,7 +119,7 @@ func (gui *Gui) switchFocusAux(newView *gocui.View) error { return gui.newLineFocused(newView) } -func (gui *Gui) returnFocus(g *gocui.Gui, v *gocui.View) error { +func (gui *Gui) returnFocus() error { gui.Mutexes.ViewStackMutex.Lock() defer gui.Mutexes.ViewStackMutex.Unlock() @@ -128,7 +128,7 @@ func (gui *Gui) returnFocus(g *gocui.Gui, v *gocui.View) error { } previousViewName := gui.State.ViewStack[len(gui.State.ViewStack)-2] - previousView, err := g.View(previousViewName) + previousView, err := gui.g.View(previousViewName) if err != nil { return err } @@ -153,6 +153,21 @@ func (gui *Gui) pushView(name string) { }) gui.State.ViewStack = append(gui.State.ViewStack, name) + gui.Log.Warnf("Just pushed %s. View stack is now "+strings.Join(gui.State.ViewStack, ", "), name) +} + +// excludes popups +func (gui *Gui) currentStaticViewName() string { + gui.Mutexes.ViewStackMutex.Lock() + defer gui.Mutexes.ViewStackMutex.Unlock() + + for i := len(gui.State.ViewStack) - 1; i >= 0; i-- { + if !lo.Contains(gui.popupViewNames(), gui.State.ViewStack[i]) { + return gui.State.ViewStack[i] + } + } + + return gui.initiallyFocusedViewName() } // if the cursor down past the last item, move it to the last line diff --git a/pkg/gui/views.go b/pkg/gui/views.go index 3de54c26..6a7ed3e6 100644 --- a/pkg/gui/views.go +++ b/pkg/gui/views.go @@ -127,3 +127,7 @@ func (gui *Gui) getInformationContent() string { donate := color.New(color.FgMagenta, color.Underline).Sprint(gui.Tr.Donate) return donate + " " + informationStr } + +func (gui *Gui) popupViewNames() []string { + return []string{"confirmation", "menu"} +} diff --git a/pkg/gui/volumes_panel.go b/pkg/gui/volumes_panel.go index 6d69e95a..d052856d 100644 --- a/pkg/gui/volumes_panel.go +++ b/pkg/gui/volumes_panel.go @@ -239,7 +239,7 @@ func (gui *Gui) handleVolumesRemoveMenu(g *gocui.Gui, v *gocui.View) error { } func (gui *Gui) handlePruneVolumes() error { - return gui.createConfirmationPanel(gui.g, gui.getVolumesView(), gui.Tr.Confirm, gui.Tr.ConfirmPruneVolumes, func(g *gocui.Gui, v *gocui.View) error { + return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.ConfirmPruneVolumes, func(g *gocui.Gui, v *gocui.View) error { return gui.WithWaitingStatus(gui.Tr.PruningStatus, func() error { err := gui.DockerCommand.PruneVolumes() if err != nil { diff --git a/pkg/gui/window.go b/pkg/gui/window.go index 42c12dd5..f596a2b2 100644 --- a/pkg/gui/window.go +++ b/pkg/gui/window.go @@ -5,3 +5,8 @@ func (gui *Gui) currentWindow() string { // are using the view name as the window name return gui.currentViewName() } + +// excludes popups +func (gui *Gui) currentStaticWindowName() string { + return gui.currentStaticViewName() +} From 3e8b594f808bc71cc05c659796c39903e35246df Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sun, 9 Oct 2022 18:01:38 -0700 Subject: [PATCH 20/42] display ports against container and service --- pkg/commands/container.go | 11 ++++++++++- pkg/commands/service.go | 4 ++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/pkg/commands/container.go b/pkg/commands/container.go index 9b56664d..2cb60854 100644 --- a/pkg/commands/container.go +++ b/pkg/commands/container.go @@ -9,6 +9,7 @@ import ( "sync" "github.com/docker/docker/api/types/container" + "github.com/samber/lo" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" @@ -51,7 +52,15 @@ type Container struct { func (c *Container) GetDisplayStrings(isFocused bool) []string { image := strings.TrimPrefix(c.Container.Image, "sha256:") - return []string{c.GetDisplayStatus(), c.GetDisplaySubstatus(), c.Name, c.GetDisplayCPUPerc(), utils.ColoredString(image, color.FgMagenta)} + return []string{c.GetDisplayStatus(), c.GetDisplaySubstatus(), c.Name, c.GetDisplayCPUPerc(), utils.ColoredString(image, color.FgMagenta), c.displayPorts()} +} + +func (c *Container) displayPorts() string { + portStrings := lo.Map(c.Container.Ports, func(port types.Port, _ int) string { + return fmt.Sprintf("%s:%d->%d/%s", port.IP, port.PublicPort, port.PrivatePort, port.Type) + }) + + return strings.Join(portStrings, ", ") } // GetDisplayStatus returns the colored status of the container diff --git a/pkg/commands/service.go b/pkg/commands/service.go index 269e119f..f45146bc 100644 --- a/pkg/commands/service.go +++ b/pkg/commands/service.go @@ -24,11 +24,11 @@ type Service struct { // GetDisplayStrings returns the dispaly string of Container func (s *Service) GetDisplayStrings(isFocused bool) []string { if s.Container == nil { - return []string{utils.ColoredString("none", color.FgBlue), "", s.Name, ""} + return []string{utils.ColoredString("none", color.FgBlue), "", s.Name, "", ""} } cont := s.Container - return []string{cont.GetDisplayStatus(), cont.GetDisplaySubstatus(), s.Name, cont.GetDisplayCPUPerc()} + return []string{cont.GetDisplayStatus(), cont.GetDisplaySubstatus(), s.Name, cont.GetDisplayCPUPerc(), utils.ColoredString(cont.displayPorts(), color.FgYellow)} } // Remove removes the service's containers From dd8b1e017c8c3bd73ccdd84274924b0cad5f26cd Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sun, 9 Oct 2022 18:10:45 -0700 Subject: [PATCH 21/42] more concise showing of ports --- pkg/commands/container.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pkg/commands/container.go b/pkg/commands/container.go index 2cb60854..c239640d 100644 --- a/pkg/commands/container.go +++ b/pkg/commands/container.go @@ -57,7 +57,14 @@ func (c *Container) GetDisplayStrings(isFocused bool) []string { func (c *Container) displayPorts() string { portStrings := lo.Map(c.Container.Ports, func(port types.Port, _ int) string { - return fmt.Sprintf("%s:%d->%d/%s", port.IP, port.PublicPort, port.PrivatePort, port.Type) + // docker ps will show '0.0.0.0:80->80/tcp' but we'll show + // '80->80/tcp' instead to save space (unless the IP is something other than + // 0.0.0.0) + ipString := "" + if port.IP != "0.0.0.0" { + ipString = port.IP + ":" + } + return fmt.Sprintf("%s%d->%d/%s", ipString, port.PublicPort, port.PrivatePort, port.Type) }) return strings.Join(portStrings, ", ") From 091e6d32ca9c875991abe83f06fe927e3a9ec285 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sun, 9 Oct 2022 18:21:33 -0700 Subject: [PATCH 22/42] clean up some things remove redundant g argument --- docs/Config.md | 8 ++++++++ pkg/config/app_config.go | 3 ++- pkg/gui/app_status_manager.go | 2 +- pkg/gui/arrangement.go | 24 ------------------------ pkg/gui/confirmation_panel.go | 6 +++--- pkg/gui/containers_panel.go | 14 +++++++------- pkg/gui/custom_commands.go | 2 +- pkg/gui/gui.go | 8 ++++---- pkg/gui/images_panel.go | 4 ++-- pkg/gui/layout.go | 2 +- pkg/gui/project_panel.go | 2 +- pkg/gui/services_panel.go | 22 +++++++++++----------- pkg/gui/subprocess.go | 14 ++++---------- pkg/gui/view_helpers.go | 17 ++++++++++++++++- pkg/gui/views.go | 12 +++++------- pkg/gui/volumes_panel.go | 4 ++-- pkg/gui/window.go | 14 +++++++++----- 17 files changed, 77 insertions(+), 81 deletions(-) diff --git a/docs/Config.md b/docs/Config.md index 92a8a3ed..fecb7d3c 100644 --- a/docs/Config.md +++ b/docs/Config.md @@ -28,6 +28,14 @@ gui: - blue returnImmediately: false wrapMainPanel: true + # Side panel width as a ratio of the screen's width + sidePanelWidth: 0.333 + # Determines whether we show the bottom line (the one containing keybinding + # info and the status of the app). + showBottomLine: true + # When true, increases vertical space used by focused side panel, + # creating an accordion effect + expandFocusedSidePanel: false logs: timestamps: false since: '60m' diff --git a/pkg/config/app_config.go b/pkg/config/app_config.go index 9094104f..78064067 100644 --- a/pkg/config/app_config.go +++ b/pkg/config/app_config.go @@ -122,7 +122,8 @@ type GuiConfig struct { // info and the status of the app). ShowBottomLine bool `yaml:"showBottomLine"` - // When true, ensures that the focused side panel always takes up the full vertical space + // When true, increases vertical space used by focused side panel, + // creating an accordion effect ExpandFocusedSidePanel bool `yaml:"expandFocusedSidePanel"` } diff --git a/pkg/gui/app_status_manager.go b/pkg/gui/app_status_manager.go index 20a81120..f806f72c 100644 --- a/pkg/gui/app_status_manager.go +++ b/pkg/gui/app_status_manager.go @@ -73,7 +73,7 @@ func (gui *Gui) WithWaitingStatus(name string, f func() error) error { if err := f(); err != nil { gui.g.Update(func(g *gocui.Gui) error { - return gui.createErrorPanel(gui.g, err.Error()) + return gui.createErrorPanel(err.Error()) }) } }() diff --git a/pkg/gui/arrangement.go b/pkg/gui/arrangement.go index df717341..f507685b 100644 --- a/pkg/gui/arrangement.go +++ b/pkg/gui/arrangement.go @@ -63,17 +63,6 @@ func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map return boxlayout.ArrangeWindows(root, 0, 0, width, height) } -func MergeMaps[K comparable, V any](maps ...map[K]V) map[K]V { - result := map[K]V{} - for _, currMap := range maps { - for key, value := range currMap { - result[key] = value - } - } - - return result -} - func (gui *Gui) getMidSectionWeights() (int, int) { currentWindow := gui.currentStaticWindowName() @@ -203,16 +192,3 @@ func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box { }) } } - -func (gui *Gui) currentSideWindowName() string { - // we expect that there is a side window somewhere in the view stack, so we will search from top to bottom - for idx := range gui.State.ViewStack { - reversedIdx := len(gui.State.ViewStack) - 1 - idx - viewName := gui.State.ViewStack[reversedIdx] - if lo.Contains(gui.sideViewNames(), viewName) { - return viewName - } - } - - return gui.initiallyFocusedViewName() -} diff --git a/pkg/gui/confirmation_panel.go b/pkg/gui/confirmation_panel.go index c515fd54..f9e1ec74 100644 --- a/pkg/gui/confirmation_panel.go +++ b/pkg/gui/confirmation_panel.go @@ -62,14 +62,14 @@ func (gui *Gui) getConfirmationPanelDimensions(g *gocui.Gui, wrap bool, prompt s height/2 + panelHeight/2 } -func (gui *Gui) createPromptPanel(g *gocui.Gui, currentView *gocui.View, title string, handleConfirm func(*gocui.Gui, *gocui.View) error) error { +func (gui *Gui) createPromptPanel(title string, handleConfirm func(*gocui.Gui, *gocui.View) error) error { gui.onNewPopupPanel() err := gui.prepareConfirmationPanel(title, "", false) if err != nil { return err } gui.Views.Confirmation.Editable = true - return gui.setKeyBindings(g, handleConfirm, nil) + return gui.setKeyBindings(gui.g, handleConfirm, nil) } func (gui *Gui) prepareConfirmationPanel(title, prompt string, hasLoader bool) error { @@ -143,7 +143,7 @@ func (gui *Gui) setKeyBindings(g *gocui.Gui, handleConfirm, handleClose func(*go return nil } -func (gui *Gui) createErrorPanel(g *gocui.Gui, message string) error { +func (gui *Gui) createErrorPanel(message string) error { colorFunction := color.New(color.FgRed).SprintFunc() coloredMessage := colorFunction(strings.TrimSpace(message)) return gui.createConfirmationPanel(gui.Tr.ErrorTitle, coloredMessage, nil, nil) diff --git a/pkg/gui/containers_panel.go b/pkg/gui/containers_panel.go index 3dc9bd2f..4f6bb941 100644 --- a/pkg/gui/containers_panel.go +++ b/pkg/gui/containers_panel.go @@ -197,7 +197,7 @@ func (gui *Gui) renderContainerStats(container *commands.Container) error { contents, err := container.RenderStats(width) if err != nil { - _ = gui.createErrorPanel(gui.g, err.Error()) + _ = gui.createErrorPanel(err.Error()) } gui.reRenderStringMain(contents) @@ -415,7 +415,7 @@ func (gui *Gui) handleContainersRemoveMenu(g *gocui.Gui, v *gocui.View) error { }) }, nil) } - return gui.createErrorPanel(gui.g, err.Error()) + return gui.createErrorPanel(err.Error()) } return nil }) @@ -433,7 +433,7 @@ func (gui *Gui) PauseContainer(container *commands.Container) error { } if err != nil { - return gui.createErrorPanel(gui.g, err.Error()) + return gui.createErrorPanel(err.Error()) } return gui.refreshContainersAndServices() @@ -458,7 +458,7 @@ func (gui *Gui) handleContainerStop(g *gocui.Gui, v *gocui.View) error { return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.StopContainer, func(g *gocui.Gui, v *gocui.View) error { return gui.WithWaitingStatus(gui.Tr.StoppingStatus, func() error { if err := container.Stop(); err != nil { - return gui.createErrorPanel(gui.g, err.Error()) + return gui.createErrorPanel(err.Error()) } return nil @@ -474,7 +474,7 @@ func (gui *Gui) handleContainerRestart(g *gocui.Gui, v *gocui.View) error { return gui.WithWaitingStatus(gui.Tr.RestartingStatus, func() error { if err := container.Restart(); err != nil { - return gui.createErrorPanel(gui.g, err.Error()) + return gui.createErrorPanel(err.Error()) } return nil @@ -489,7 +489,7 @@ func (gui *Gui) handleContainerAttach(g *gocui.Gui, v *gocui.View) error { c, err := container.Attach() if err != nil { - return gui.createErrorPanel(gui.g, err.Error()) + return gui.createErrorPanel(err.Error()) } return gui.runSubprocess(c) @@ -500,7 +500,7 @@ func (gui *Gui) handlePruneContainers() error { return gui.WithWaitingStatus(gui.Tr.PruningStatus, func() error { err := gui.DockerCommand.PruneContainers() if err != nil { - return gui.createErrorPanel(gui.g, err.Error()) + return gui.createErrorPanel(err.Error()) } return nil }) diff --git a/pkg/gui/custom_commands.go b/pkg/gui/custom_commands.go index da03eed9..6038b6b2 100644 --- a/pkg/gui/custom_commands.go +++ b/pkg/gui/custom_commands.go @@ -58,7 +58,7 @@ func (gui *Gui) createCommandMenu(customCommands []config.CustomCommand, command return gui.WithWaitingStatus(waitingStatus, func() error { if err := gui.OSCommand.RunCommand(option.command); err != nil { - return gui.createErrorPanel(gui.g, err.Error()) + return gui.createErrorPanel(err.Error()) } return nil }) diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 94aa4c0d..69245453 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -269,7 +269,7 @@ func (gui *Gui) Run() error { gui.Log.Warn(err) continue } - _ = gui.createErrorPanel(gui.g, err.Error()) + _ = gui.createErrorPanel(err.Error()) } }() @@ -407,7 +407,7 @@ func (gui *Gui) handleDonate(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) editFile(filename string) error { cmd, err := gui.OSCommand.EditFile(filename) if err != nil { - return gui.createErrorPanel(gui.g, err.Error()) + return gui.createErrorPanel(err.Error()) } return gui.runSubprocess(cmd) @@ -415,13 +415,13 @@ func (gui *Gui) editFile(filename string) error { func (gui *Gui) openFile(filename string) error { if err := gui.OSCommand.OpenFile(filename); err != nil { - return gui.createErrorPanel(gui.g, err.Error()) + return gui.createErrorPanel(err.Error()) } return nil } func (gui *Gui) handleCustomCommand(g *gocui.Gui, v *gocui.View) error { - return gui.createPromptPanel(g, v, gui.Tr.CustomCommandTitle, func(g *gocui.Gui, v *gocui.View) error { + return gui.createPromptPanel(gui.Tr.CustomCommandTitle, func(g *gocui.Gui, v *gocui.View) error { command := gui.trimmedContent(v) return gui.runSubprocess(gui.OSCommand.RunCustomCommand(command)) }) diff --git a/pkg/gui/images_panel.go b/pkg/gui/images_panel.go index 2f513ab5..9e2bd8cb 100644 --- a/pkg/gui/images_panel.go +++ b/pkg/gui/images_panel.go @@ -250,7 +250,7 @@ func (gui *Gui) handleImagesRemoveMenu(g *gocui.Gui, v *gocui.View) error { } configOptions := options[index].configOptions if cerr := Image.Remove(configOptions); cerr != nil { - return gui.createErrorPanel(gui.g, cerr.Error()) + return gui.createErrorPanel(cerr.Error()) } return nil @@ -264,7 +264,7 @@ func (gui *Gui) handlePruneImages() error { return gui.WithWaitingStatus(gui.Tr.PruningStatus, func() error { err := gui.DockerCommand.PruneImages() if err != nil { - return gui.createErrorPanel(gui.g, err.Error()) + return gui.createErrorPanel(err.Error()) } return gui.refreshImages() }) diff --git a/pkg/gui/layout.go b/pkg/gui/layout.go index 11e86758..134076ce 100644 --- a/pkg/gui/layout.go +++ b/pkg/gui/layout.go @@ -109,7 +109,7 @@ func (gui *Gui) layout(g *gocui.Gui) error { return view, err } - for _, viewName := range []string{"project", "services", "containers", "images", "volumes", "options", "information", "appStatus", "main", "limit"} { + for _, viewName := range gui.controlledBoundsViewNames() { _, err := setViewFromDimensions(viewName, viewName) if err != nil && err.Error() != UNKNOWN_VIEW_ERROR_MSG { return err diff --git a/pkg/gui/project_panel.go b/pkg/gui/project_panel.go index 3dd14175..0ae98e9d 100644 --- a/pkg/gui/project_panel.go +++ b/pkg/gui/project_panel.go @@ -221,7 +221,7 @@ func (gui *Gui) handleProjectPrevContext(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleViewAllLogs(g *gocui.Gui, v *gocui.View) error { c, err := gui.DockerCommand.ViewAllLogs() if err != nil { - return gui.createErrorPanel(gui.g, err.Error()) + return gui.createErrorPanel(err.Error()) } return gui.runSubprocess(c) diff --git a/pkg/gui/services_panel.go b/pkg/gui/services_panel.go index 16accf28..76d0116b 100644 --- a/pkg/gui/services_panel.go +++ b/pkg/gui/services_panel.go @@ -240,7 +240,7 @@ func (gui *Gui) handleServiceStop(g *gocui.Gui, v *gocui.View) error { return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.StopService, func(g *gocui.Gui, v *gocui.View) error { return gui.WithWaitingStatus(gui.Tr.StoppingStatus, func() error { if err := service.Stop(); err != nil { - return gui.createErrorPanel(gui.g, err.Error()) + return gui.createErrorPanel(err.Error()) } return nil @@ -256,7 +256,7 @@ func (gui *Gui) handleServiceRestart(g *gocui.Gui, v *gocui.View) error { return gui.WithWaitingStatus(gui.Tr.RestartingStatus, func() error { if err := service.Restart(); err != nil { - return gui.createErrorPanel(gui.g, err.Error()) + return gui.createErrorPanel(err.Error()) } return nil @@ -271,7 +271,7 @@ func (gui *Gui) handleServiceStart(g *gocui.Gui, v *gocui.View) error { return gui.WithWaitingStatus(gui.Tr.StartingStatus, func() error { if err := service.Start(); err != nil { - return gui.createErrorPanel(gui.g, err.Error()) + return gui.createErrorPanel(err.Error()) } return nil @@ -285,12 +285,12 @@ func (gui *Gui) handleServiceAttach(g *gocui.Gui, v *gocui.View) error { } if service.Container == nil { - return gui.createErrorPanel(gui.g, gui.Tr.NoContainers) + return gui.createErrorPanel(gui.Tr.NoContainers) } c, err := service.Attach() if err != nil { - return gui.createErrorPanel(gui.g, err.Error()) + return gui.createErrorPanel(err.Error()) } return gui.runSubprocess(c) @@ -304,7 +304,7 @@ func (gui *Gui) handleServiceRenderLogsToMain(g *gocui.Gui, v *gocui.View) error c, err := service.ViewLogs() if err != nil { - return gui.createErrorPanel(gui.g, err.Error()) + return gui.createErrorPanel(err.Error()) } return gui.runSubprocess(c) @@ -336,7 +336,7 @@ func (gui *Gui) handleServiceRestartMenu(g *gocui.Gui, v *gocui.View) error { f: func() error { return gui.WithWaitingStatus(gui.Tr.RestartingStatus, func() error { if err := service.Restart(); err != nil { - return gui.createErrorPanel(gui.g, err.Error()) + return gui.createErrorPanel(err.Error()) } return nil }) @@ -351,7 +351,7 @@ func (gui *Gui) handleServiceRestartMenu(g *gocui.Gui, v *gocui.View) error { f: func() error { return gui.WithWaitingStatus(gui.Tr.RestartingStatus, func() error { if err := gui.OSCommand.RunCommand(recreateCommand); err != nil { - return gui.createErrorPanel(gui.g, err.Error()) + return gui.createErrorPanel(err.Error()) } return nil }) @@ -385,7 +385,7 @@ func (gui *Gui) createServiceCommandMenu(options []*commandOption, status string } return gui.WithWaitingStatus(status, func() error { if err := gui.OSCommand.RunCommand(options[index].command); err != nil { - return gui.createErrorPanel(gui.g, err.Error()) + return gui.createErrorPanel(err.Error()) } return nil @@ -447,7 +447,7 @@ func (gui *Gui) handleServicesExecShell(g *gocui.Gui, v *gocui.View) error { container := service.Container if container == nil { - return gui.createErrorPanel(gui.g, gui.Tr.NoContainers) + return gui.createErrorPanel(gui.Tr.NoContainers) } return gui.containerExecShell(container) @@ -461,7 +461,7 @@ func (gui *Gui) handleServicesOpenInBrowserCommand(g *gocui.Gui, v *gocui.View) container := service.Container if container == nil { - return gui.createErrorPanel(gui.g, gui.Tr.NoContainers) + return gui.createErrorPanel(gui.Tr.NoContainers) } return gui.openContainerInBrowser(container) diff --git a/pkg/gui/subprocess.go b/pkg/gui/subprocess.go index 54af269d..8366393c 100644 --- a/pkg/gui/subprocess.go +++ b/pkg/gui/subprocess.go @@ -17,27 +17,23 @@ func (gui *Gui) runSubprocess(cmd *exec.Cmd) error { defer gui.Mutexes.SubprocessMutex.Unlock() if err := gui.g.Suspend(); err != nil { - return gui.createErrorPanel(gui.g, err.Error()) + return gui.createErrorPanel(err.Error()) } gui.PauseBackgroundThreads = true - cmdErr := gui.runCommand(cmd) + gui.runCommand(cmd) if err := gui.g.Resume(); err != nil { - return gui.createErrorPanel(gui.g, err.Error()) + return gui.createErrorPanel(err.Error()) } gui.PauseBackgroundThreads = false - if cmdErr != nil { - return gui.createErrorPanel(gui.g, cmdErr.Error()) - } - return nil } -func (gui *Gui) runCommand(cmd *exec.Cmd) error { +func (gui *Gui) runCommand(cmd *exec.Cmd) { cmd.Stdout = os.Stdout cmd.Stderr = os.Stdout cmd.Stdin = os.Stdin @@ -67,6 +63,4 @@ func (gui *Gui) runCommand(cmd *exec.Cmd) error { cmd.Stderr = ioutil.Discard gui.promptToReturn() - - return nil } diff --git a/pkg/gui/view_helpers.go b/pkg/gui/view_helpers.go index 740ccbc1..000118d3 100644 --- a/pkg/gui/view_helpers.go +++ b/pkg/gui/view_helpers.go @@ -153,7 +153,6 @@ func (gui *Gui) pushView(name string) { }) gui.State.ViewStack = append(gui.State.ViewStack, name) - gui.Log.Warnf("Just pushed %s. View stack is now "+strings.Join(gui.State.ViewStack, ", "), name) } // excludes popups @@ -170,6 +169,22 @@ func (gui *Gui) currentStaticViewName() string { return gui.initiallyFocusedViewName() } +func (gui *Gui) currentSideViewName() string { + gui.Mutexes.ViewStackMutex.Lock() + defer gui.Mutexes.ViewStackMutex.Unlock() + + // we expect that there is a side window somewhere in the view stack, so we will search from top to bottom + for idx := range gui.State.ViewStack { + reversedIdx := len(gui.State.ViewStack) - 1 - idx + viewName := gui.State.ViewStack[reversedIdx] + if lo.Contains(gui.sideViewNames(), viewName) { + return viewName + } + } + + return gui.initiallyFocusedViewName() +} + // if the cursor down past the last item, move it to the last line // nolint:unparam func (gui *Gui) focusPoint(selectedX int, selectedY int, lineCount int, v *gocui.View) { diff --git a/pkg/gui/views.go b/pkg/gui/views.go index 6a7ed3e6..4b568ed0 100644 --- a/pkg/gui/views.go +++ b/pkg/gui/views.go @@ -3,7 +3,6 @@ package gui import ( "github.com/fatih/color" "github.com/jesseduffield/gocui" - "github.com/samber/lo" ) type Views struct { @@ -28,12 +27,6 @@ type viewNameMapping struct { name string } -func (gui *Gui) orderedViews() []*gocui.View { - return lo.Map(gui.orderedViewNameMappings(), func(v viewNameMapping, _ int) *gocui.View { - return *v.viewPtr - }) -} - func (gui *Gui) orderedViewNameMappings() []viewNameMapping { return []viewNameMapping{ // first layer. Ordering within this layer does not matter because there are @@ -131,3 +124,8 @@ func (gui *Gui) getInformationContent() string { func (gui *Gui) popupViewNames() []string { return []string{"confirmation", "menu"} } + +// these views have their position and size determined by arrangement.go +func (gui *Gui) controlledBoundsViewNames() []string { + return []string{"project", "services", "containers", "images", "volumes", "options", "information", "appStatus", "main", "limit"} +} diff --git a/pkg/gui/volumes_panel.go b/pkg/gui/volumes_panel.go index d052856d..7d2cf936 100644 --- a/pkg/gui/volumes_panel.go +++ b/pkg/gui/volumes_panel.go @@ -229,7 +229,7 @@ func (gui *Gui) handleVolumesRemoveMenu(g *gocui.Gui, v *gocui.View) error { } return gui.WithWaitingStatus(gui.Tr.RemovingStatus, func() error { if cerr := volume.Remove(options[index].force); cerr != nil { - return gui.createErrorPanel(gui.g, cerr.Error()) + return gui.createErrorPanel(cerr.Error()) } return nil }) @@ -243,7 +243,7 @@ func (gui *Gui) handlePruneVolumes() error { return gui.WithWaitingStatus(gui.Tr.PruningStatus, func() error { err := gui.DockerCommand.PruneVolumes() if err != nil { - return gui.createErrorPanel(gui.g, err.Error()) + return gui.createErrorPanel(err.Error()) } return nil }) diff --git a/pkg/gui/window.go b/pkg/gui/window.go index f596a2b2..d58d132f 100644 --- a/pkg/gui/window.go +++ b/pkg/gui/window.go @@ -1,12 +1,16 @@ package gui -func (gui *Gui) currentWindow() string { - // at the moment, we only have one view per window in lazydocker, so we - // are using the view name as the window name - return gui.currentViewName() -} +// func (gui *Gui) currentWindow() string { +// // at the moment, we only have one view per window in lazydocker, so we +// // are using the view name as the window name +// return gui.currentViewName() +// } // excludes popups func (gui *Gui) currentStaticWindowName() string { return gui.currentStaticViewName() } + +func (gui *Gui) currentSideWindowName() string { + return gui.currentSideViewName() +} From 413250cbc2c7787cdbd38ba0f0133468851d9fe1 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sun, 9 Oct 2022 18:43:19 -0700 Subject: [PATCH 23/42] show selected line in blue --- pkg/gui/views.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pkg/gui/views.go b/pkg/gui/views.go index 4b568ed0..bf417cbe 100644 --- a/pkg/gui/views.go +++ b/pkg/gui/views.go @@ -63,6 +63,8 @@ func (gui *Gui) createAllViews() error { (*mapping.viewPtr).FgColor = gocui.ColorDefault } + selectedLineBgColor := gocui.ColorBlue + gui.Views.Main.Wrap = gui.Config.UserConfig.Gui.WrapMainPanel // when you run a docker container with the -it flags (interactive mode) it adds carriage returns for some reason. This is not docker's fault, it's an os-level default. gui.Views.Main.IgnoreCarriageReturns = true @@ -71,8 +73,10 @@ func (gui *Gui) createAllViews() error { gui.Views.Services.Highlight = true gui.Views.Services.Title = gui.Tr.ServicesTitle + gui.Views.Services.SelBgColor = selectedLineBgColor gui.Views.Containers.Highlight = true + gui.Views.Containers.SelBgColor = selectedLineBgColor if gui.Config.UserConfig.Gui.ShowAllContainers || !gui.DockerCommand.InDockerComposeProject { gui.Views.Containers.Title = gui.Tr.ContainersTitle } else { @@ -81,9 +85,11 @@ func (gui *Gui) createAllViews() error { gui.Views.Images.Highlight = true gui.Views.Images.Title = gui.Tr.ImagesTitle + gui.Views.Images.SelBgColor = selectedLineBgColor gui.Views.Volumes.Highlight = true gui.Views.Volumes.Title = gui.Tr.VolumesTitle + gui.Views.Volumes.SelBgColor = selectedLineBgColor gui.Views.Options.Frame = false gui.Views.Options.FgColor = gui.GetOptionsPanelTextColor() @@ -101,6 +107,7 @@ func (gui *Gui) createAllViews() error { gui.Views.Confirmation.Visible = false gui.Views.Confirmation.Wrap = true gui.Views.Menu.Visible = false + gui.Views.Menu.SelBgColor = selectedLineBgColor gui.Views.Limit.Visible = false gui.Views.Limit.Title = gui.Tr.NotEnoughSpace From 2eaf3c099e16aa8be2cd85e59445b2c434fb46d4 Mon Sep 17 00:00:00 2001 From: README-bot Date: Mon, 10 Oct 2022 02:07:39 +0000 Subject: [PATCH 24/42] Updated README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8596f5ae..4b9caa03 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ A simple terminal UI for both docker and docker-compose, written in Go with the

- +

## Elevator Pitch From 0024184ba2bcb4a386c5624e58043a46fee96d6f Mon Sep 17 00:00:00 2001 From: Gustavo Andrioli Date: Mon, 10 Oct 2022 15:36:18 -0300 Subject: [PATCH 25/42] Add message that cheetsheets are auto-generated --- docs/keybindings/Keybindings_de.md | 17 +++++++++++++++-- docs/keybindings/Keybindings_en.md | 15 ++++++++++++++- docs/keybindings/Keybindings_nl.md | 15 ++++++++++++++- docs/keybindings/Keybindings_pl.md | 15 ++++++++++++++- docs/keybindings/Keybindings_tr.md | 15 ++++++++++++++- scripts/generate_cheatsheet.go | 10 ++++++++++ 6 files changed, 81 insertions(+), 6 deletions(-) diff --git a/docs/keybindings/Keybindings_de.md b/docs/keybindings/Keybindings_de.md index ea89147d..3258e52a 100644 --- a/docs/keybindings/Keybindings_de.md +++ b/docs/keybindings/Keybindings_de.md @@ -1,4 +1,6 @@ -# Lazydocker Menü +_This file is auto-generated. To update, make the changes in the pkg/i18n directory and then run `go run scripts/generate_cheatsheet.go` from the project root._ + +# Lazydocker menü ## Projekt @@ -17,7 +19,8 @@ [: vorheriges Tab ]: nächstes Tab d: entfernen - e: Hide/Show stopped containers + e: hide/show stopped containers + p: pause s: anhalten r: neustarten a: anbinden @@ -34,7 +37,9 @@
   d: entferne Container
   s: anhalten
+  p: pause
   r: neustarten
+  S: start
   a: anbinden
   m: zeige Protokolle
   [: vorheriges Tab
@@ -42,6 +47,7 @@
   R: zeige Neustartoptionen
   c: führe vordefinierten benutzerdefinierten Befehl aus
   b: view bulk commands
+  E: exec shell
   w: open in browser (first port is http)
   enter: fokussieren aufs Hauptpanel
 
@@ -73,3 +79,10 @@
   esc: zurück
 
+ +## Global + +
+  +: next screen mode (normal/half/fullscreen)
+  _: prev screen mode
+
diff --git a/docs/keybindings/Keybindings_en.md b/docs/keybindings/Keybindings_en.md index 85e67026..db306cda 100644 --- a/docs/keybindings/Keybindings_en.md +++ b/docs/keybindings/Keybindings_en.md @@ -1,3 +1,5 @@ +_This file is auto-generated. To update, make the changes in the pkg/i18n directory and then run `go run scripts/generate_cheatsheet.go` from the project root._ + # Lazydocker menu ## Project @@ -17,7 +19,8 @@ [: previous tab ]: next tab d: remove - e: Hide/Show stopped containers + e: hide/show stopped containers + p: pause s: stop r: restart a: attach @@ -34,7 +37,9 @@
   d: remove containers
   s: stop
+  p: pause
   r: restart
+  S: start
   a: attach
   m: view logs
   [: previous tab
@@ -42,6 +47,7 @@
   R: view restart options
   c: run predefined custom command
   b: view bulk commands
+  E: exec shell
   w: open in browser (first port is http)
   enter: focus main panel
 
@@ -73,3 +79,10 @@
   esc: return
 
+ +## Global + +
+  +: next screen mode (normal/half/fullscreen)
+  _: prev screen mode
+
diff --git a/docs/keybindings/Keybindings_nl.md b/docs/keybindings/Keybindings_nl.md index f0f50dbc..6f86660b 100644 --- a/docs/keybindings/Keybindings_nl.md +++ b/docs/keybindings/Keybindings_nl.md @@ -1,3 +1,5 @@ +_This file is auto-generated. To update, make the changes in the pkg/i18n directory and then run `go run scripts/generate_cheatsheet.go` from the project root._ + # Lazydocker menu ## Project @@ -17,7 +19,8 @@ [: vorige tab ]: volgende tab d: verwijder - e: Verberg gestopte containers + e: verberg gestopte containers + p: pause s: stop r: herstart a: verbinden @@ -34,7 +37,9 @@
   d: verwijder containers
   s: stop
+  p: pause
   r: herstart
+  S: start
   a: verbinden
   m: bekijk logs
   [: vorige tab
@@ -42,6 +47,7 @@
   R: bekijk herstart opties
   c: draai een vooraf bedacht aangepaste opdracht
   b: view bulk commands
+  E: exec shell
   w: open in browser (first port is http)
   enter: focus hoofdpaneel
 
@@ -73,3 +79,10 @@
   esc: terug
 
+ +## Globaal + +
+  +: next screen mode (normal/half/fullscreen)
+  _: prev screen mode
+
diff --git a/docs/keybindings/Keybindings_pl.md b/docs/keybindings/Keybindings_pl.md index a902386f..921b3db8 100644 --- a/docs/keybindings/Keybindings_pl.md +++ b/docs/keybindings/Keybindings_pl.md @@ -1,3 +1,5 @@ +_This file is auto-generated. To update, make the changes in the pkg/i18n directory and then run `go run scripts/generate_cheatsheet.go` from the project root._ + # Lazydocker menu ## Projekt @@ -17,7 +19,8 @@ [: poprzednia zakładka ]: następna zakładka d: usuń - e: Hide/Show stopped containers + e: hide/show stopped containers + p: pause s: zatrzymaj r: restartuj a: przyczep @@ -34,7 +37,9 @@
   d: usuń kontenery
   s: zatrzymaj
+  p: pause
   r: restartuj
+  S: start
   a: przyczep
   m: pokaż logi
   [: poprzednia zakładka
@@ -42,6 +47,7 @@
   R: pokaż opcje restartu
   c: wykonaj predefiniowaną własną komende
   b: view bulk commands
+  E: exec shell
   w: open in browser (first port is http)
   enter: skup na głównym panelu
 
@@ -73,3 +79,10 @@
   esc: powrót
 
+ +## Globalne + +
+  +: next screen mode (normal/half/fullscreen)
+  _: prev screen mode
+
diff --git a/docs/keybindings/Keybindings_tr.md b/docs/keybindings/Keybindings_tr.md index 85b43165..4c16e232 100644 --- a/docs/keybindings/Keybindings_tr.md +++ b/docs/keybindings/Keybindings_tr.md @@ -1,3 +1,5 @@ +_This file is auto-generated. To update, make the changes in the pkg/i18n directory and then run `go run scripts/generate_cheatsheet.go` from the project root._ + # Lazydocker menü ## Proje @@ -17,7 +19,8 @@ [: önceki sekme ]: sonraki sekme d: kaldır - e: Hide/Show stopped containers + e: hide/show stopped containers + p: pause s: durdur r: yeniden başlat a: bağlan/iliştir @@ -34,7 +37,9 @@
   d: konteynerleri kaldır
   s: durdur
+  p: pause
   r: yeniden başlat
+  S: start
   a: bağlan/iliştir
   m: kayıt defterini görüntüle
   [: önceki sekme
@@ -42,6 +47,7 @@
   R: yeniden başlatma seçeneklerini görüntüle
   c: önceden tanımlanmış özel komutu çalıştır
   b: view bulk commands
+  E: exec shell
   w: open in browser (first port is http)
   enter: ana panele odaklan
 
@@ -73,3 +79,10 @@
   esc: dönüş
 
+ +## Global + +
+  +: next screen mode (normal/half/fullscreen)
+  _: prev screen mode
+
diff --git a/scripts/generate_cheatsheet.go b/scripts/generate_cheatsheet.go index c1237701..79893b74 100644 --- a/scripts/generate_cheatsheet.go +++ b/scripts/generate_cheatsheet.go @@ -40,6 +40,12 @@ func main() { bindingSections := getBindingSections(mApp) content := formatSections(mApp, bindingSections) + content = fmt.Sprintf( + "_This file is auto-generated. To update, make the changes in the "+ + "pkg/i18n directory and then run `%s` from the project root._\n\n%s", + commandToRun(), + content, + ) writeString(file, content) } } @@ -134,3 +140,7 @@ func formatSections(mApp *app.App, bindingSections []*bindingSection) string { return content } + +func commandToRun() string { + return "go run scripts/generate_cheatsheet.go" +} From a5eece155c3d5ab71f6e13edd4a3fe516fe12c22 Mon Sep 17 00:00:00 2001 From: Gustavo Andrioli Date: Mon, 10 Oct 2022 16:03:24 -0300 Subject: [PATCH 26/42] Convert generate cmd func to const --- scripts/generate_cheatsheet.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/generate_cheatsheet.go b/scripts/generate_cheatsheet.go index 79893b74..67b51f07 100644 --- a/scripts/generate_cheatsheet.go +++ b/scripts/generate_cheatsheet.go @@ -19,6 +19,10 @@ import ( "github.com/jesseduffield/lazydocker/pkg/i18n" ) +const ( + generateCheatsheetCmd = "go run scripts/generate_cheatsheet.go" +) + type bindingSection struct { title string bindings []*gui.Binding @@ -43,7 +47,7 @@ func main() { content = fmt.Sprintf( "_This file is auto-generated. To update, make the changes in the "+ "pkg/i18n directory and then run `%s` from the project root._\n\n%s", - commandToRun(), + generateCheatsheetCmd, content, ) writeString(file, content) @@ -140,7 +144,3 @@ func formatSections(mApp *app.App, bindingSections []*bindingSection) string { return content } - -func commandToRun() string { - return "go run scripts/generate_cheatsheet.go" -} From 6f27ac3c64c0b00b12850c6f756b375bda18f0e2 Mon Sep 17 00:00:00 2001 From: README-bot Date: Mon, 10 Oct 2022 20:37:20 +0000 Subject: [PATCH 27/42] Updated README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4b9caa03..cf41b296 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ A simple terminal UI for both docker and docker-compose, written in Go with the

- +

## Elevator Pitch From 073385492361e4daf892d4c1c2077e40492e4e85 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Mon, 10 Oct 2022 13:45:25 -0700 Subject: [PATCH 28/42] sort volumes first on whether they have labels and secondly on name --- pkg/commands/volume.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pkg/commands/volume.go b/pkg/commands/volume.go index af44e075..75419915 100644 --- a/pkg/commands/volume.go +++ b/pkg/commands/volume.go @@ -36,7 +36,16 @@ func (c *DockerCommand) RefreshVolumes() error { ownVolumes := make([]*Volume, len(volumes)) + // we're sorting these volumes based on whether they have labels defined, + // because those are the ones you typically care about. + // Within that, we also sort them alphabetically sort.Slice(volumes, func(i, j int) bool { + if len(volumes[i].Labels) == 0 && len(volumes[j].Labels) > 0 { + return false + } + if len(volumes[i].Labels) > 0 && len(volumes[j].Labels) == 0 { + return true + } return volumes[i].Name < volumes[j].Name }) From 509da9d6fbc6900a15fce3521bbc9b0b3ac7e752 Mon Sep 17 00:00:00 2001 From: Gustavo Andrioli Date: Mon, 10 Oct 2022 17:17:28 -0300 Subject: [PATCH 29/42] Check if cheatsheets are up to date on CI --- .github/workflows/ci.yml | 3 + docs/keybindings/Keybindings_de.md | 2 +- docs/keybindings/Keybindings_en.md | 2 +- docs/keybindings/Keybindings_nl.md | 2 +- docs/keybindings/Keybindings_pl.md | 2 +- docs/keybindings/Keybindings_tr.md | 2 +- .../cheatsheet/generate.go | 13 ++- pkg/cheatsheet/validate.go | 87 +++++++++++++++++++ pkg/utils/utils.go | 31 +++++++ scripts/cheatsheet/main.go | 27 ++++++ 10 files changed, 162 insertions(+), 9 deletions(-) rename scripts/generate_cheatsheet.go => pkg/cheatsheet/generate.go (93%) create mode 100644 pkg/cheatsheet/validate.go create mode 100644 scripts/cheatsheet/main.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index deab1a1e..035f4b2e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,6 +35,9 @@ jobs: key: ${{runner.os}}-go-${{hashFiles('**/go.sum')}}-test restore-keys: | ${{runner.os}}-go- + - name: Check Cheatsheet + run: | + go run scripts/cheatsheet/main.go check - name: Test code run: | bash ./test.sh diff --git a/docs/keybindings/Keybindings_de.md b/docs/keybindings/Keybindings_de.md index 3258e52a..458cf793 100644 --- a/docs/keybindings/Keybindings_de.md +++ b/docs/keybindings/Keybindings_de.md @@ -1,4 +1,4 @@ -_This file is auto-generated. To update, make the changes in the pkg/i18n directory and then run `go run scripts/generate_cheatsheet.go` from the project root._ +_This file is auto-generated. To update, make the changes in the pkg/i18n directory and then run `go run scripts/cheatsheet/main.go generate` from the project root._ # Lazydocker menü diff --git a/docs/keybindings/Keybindings_en.md b/docs/keybindings/Keybindings_en.md index db306cda..dd0925e2 100644 --- a/docs/keybindings/Keybindings_en.md +++ b/docs/keybindings/Keybindings_en.md @@ -1,4 +1,4 @@ -_This file is auto-generated. To update, make the changes in the pkg/i18n directory and then run `go run scripts/generate_cheatsheet.go` from the project root._ +_This file is auto-generated. To update, make the changes in the pkg/i18n directory and then run `go run scripts/cheatsheet/main.go generate` from the project root._ # Lazydocker menu diff --git a/docs/keybindings/Keybindings_nl.md b/docs/keybindings/Keybindings_nl.md index 6f86660b..00dc7f88 100644 --- a/docs/keybindings/Keybindings_nl.md +++ b/docs/keybindings/Keybindings_nl.md @@ -1,4 +1,4 @@ -_This file is auto-generated. To update, make the changes in the pkg/i18n directory and then run `go run scripts/generate_cheatsheet.go` from the project root._ +_This file is auto-generated. To update, make the changes in the pkg/i18n directory and then run `go run scripts/cheatsheet/main.go generate` from the project root._ # Lazydocker menu diff --git a/docs/keybindings/Keybindings_pl.md b/docs/keybindings/Keybindings_pl.md index 921b3db8..174beb15 100644 --- a/docs/keybindings/Keybindings_pl.md +++ b/docs/keybindings/Keybindings_pl.md @@ -1,4 +1,4 @@ -_This file is auto-generated. To update, make the changes in the pkg/i18n directory and then run `go run scripts/generate_cheatsheet.go` from the project root._ +_This file is auto-generated. To update, make the changes in the pkg/i18n directory and then run `go run scripts/cheatsheet/main.go generate` from the project root._ # Lazydocker menu diff --git a/docs/keybindings/Keybindings_tr.md b/docs/keybindings/Keybindings_tr.md index 4c16e232..bb83faee 100644 --- a/docs/keybindings/Keybindings_tr.md +++ b/docs/keybindings/Keybindings_tr.md @@ -1,4 +1,4 @@ -_This file is auto-generated. To update, make the changes in the pkg/i18n directory and then run `go run scripts/generate_cheatsheet.go` from the project root._ +_This file is auto-generated. To update, make the changes in the pkg/i18n directory and then run `go run scripts/cheatsheet/main.go generate` from the project root._ # Lazydocker menü diff --git a/scripts/generate_cheatsheet.go b/pkg/cheatsheet/generate.go similarity index 93% rename from scripts/generate_cheatsheet.go rename to pkg/cheatsheet/generate.go index 67b51f07..19dff300 100644 --- a/scripts/generate_cheatsheet.go +++ b/pkg/cheatsheet/generate.go @@ -6,7 +6,7 @@ // To generate cheatsheet in english run: // LANG=en go run scripts/generate_cheatsheet.go -package main +package cheatsheet import ( "fmt" @@ -20,7 +20,7 @@ import ( ) const ( - generateCheatsheetCmd = "go run scripts/generate_cheatsheet.go" + generateCheatsheetCmd = "go run scripts/cheatsheet/main.go generate" ) type bindingSection struct { @@ -28,7 +28,11 @@ type bindingSection struct { bindings []*gui.Binding } -func main() { +func Generate() { + generateAtDir(GetKeybindingsDir()) +} + +func generateAtDir(dir string) { mConfig, err := config.NewAppConfig("lazydocker", "", "", "", "", true, nil, "") if err != nil { panic(err) @@ -37,7 +41,8 @@ func main() { for lang := range i18n.GetTranslationSets() { os.Setenv("LC_ALL", lang) mApp, _ := app.NewApp(mConfig) - file, err := os.Create("./docs/keybindings/Keybindings_" + lang + ".md") + + file, err := os.Create(dir + "/Keybindings_" + lang + ".md") if err != nil { panic(err) } diff --git a/pkg/cheatsheet/validate.go b/pkg/cheatsheet/validate.go new file mode 100644 index 00000000..1ad33681 --- /dev/null +++ b/pkg/cheatsheet/validate.go @@ -0,0 +1,87 @@ +package cheatsheet + +import ( + "fmt" + "io/fs" + "log" + "os" + "path/filepath" + "regexp" + + "github.com/jesseduffield/lazydocker/pkg/utils" + "github.com/pmezard/go-difflib/difflib" +) + +func Check() { + dir := GetKeybindingsDir() + tmpDir := filepath.Join(os.TempDir(), "lazydocker_cheatsheet") + + err := os.RemoveAll(tmpDir) + if err != nil { + log.Fatalf("Error occurred while checking if cheatsheets are up to date: %v", err) + } + defer os.RemoveAll(tmpDir) + + if err = os.Mkdir(tmpDir, 0o700); err != nil { + log.Fatalf("Error occurred while checking if cheatsheets are up to date: %v", err) + } + + generateAtDir(tmpDir) + + actualContent := obtainContent(dir) + expectedContent := obtainContent(tmpDir) + + if expectedContent == "" { + log.Fatal("empty expected content") + } + + if actualContent != expectedContent { + if err := difflib.WriteUnifiedDiff(os.Stdout, difflib.UnifiedDiff{ + A: difflib.SplitLines(expectedContent), + B: difflib.SplitLines(actualContent), + FromFile: "Expected", + FromDate: "", + ToFile: "Actual", + ToDate: "", + Context: 1, + }); err != nil { + log.Fatalf("Error occurred while checking if cheatsheets are up to date: %v", err) + } + fmt.Printf( + "\nCheatsheets are out of date. Please run `%s` at the project root and commit the changes. "+ + "If you run the script and no keybindings files are updated as a result, try rebasing onto master"+ + "and trying again.\n", + generateCheatsheetCmd, + ) + os.Exit(1) + } + + fmt.Println("\nCheatsheets are up to date") +} + +func GetKeybindingsDir() string { + return utils.GetLazydockerRootDirectory() + "/docs/keybindings" +} + +func obtainContent(dir string) string { + re := regexp.MustCompile(`Keybindings_\w+\.md$`) + + content := "" + err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error { + if re.MatchString(path) { + bytes, err := os.ReadFile(path) + if err != nil { + log.Fatalf("Error occurred while checking if cheatsheets are up to date: %v", err) + } + content += fmt.Sprintf("\n%s\n\n", filepath.Base(path)) + content += string(bytes) + } + + return nil + }) + if err != nil { + log.Fatalf("Error occurred while checking if cheatsheets are up to date: %v", err) + } + + return content +} diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index fe1ae3de..baf5cf5c 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -5,7 +5,10 @@ import ( "fmt" "html/template" "io" + "log" "math" + "os" + "path/filepath" "reflect" "regexp" "sort" @@ -408,3 +411,31 @@ func IsValidHexValue(v string) bool { return true } + +// GetLazydockerRootDirectory finds lazydocker root directory. +// +// It's used for our cheatsheet script and integration tests. Not to be confused with finding the +// root directory of _any_ random repo. +func GetLazydockerRootDirectory() string { + path, err := os.Getwd() + if err != nil { + panic(err) + } + + for { + _, err := os.Stat(filepath.Join(path, ".git")) + if err == nil { + return path + } + + if !os.IsNotExist(err) { + panic(err) + } + + path = filepath.Dir(path) + + if path == "/" { + log.Fatal("must run in lazydocker folder or child folder") + } + } +} diff --git a/scripts/cheatsheet/main.go b/scripts/cheatsheet/main.go new file mode 100644 index 00000000..53f72eda --- /dev/null +++ b/scripts/cheatsheet/main.go @@ -0,0 +1,27 @@ +package main + +import ( + "fmt" + "log" + "os" + + "github.com/jesseduffield/lazydocker/pkg/cheatsheet" +) + +func main() { + if len(os.Args) < 2 { + log.Fatal("Please provide a command: one of 'generate', 'check'") + } + + command := os.Args[1] + + switch command { + case "generate": + cheatsheet.Generate() + fmt.Printf("\nGenerated cheatsheets in %s\n", cheatsheet.GetKeybindingsDir()) + case "check": + cheatsheet.Check() + default: + log.Fatal("\nUnknown command. Expected one of 'generate', 'check'") + } +} From ff683432901c2fe8cc109eb19330daa300fab6a1 Mon Sep 17 00:00:00 2001 From: Gustavo Andrioli Date: Mon, 10 Oct 2022 17:29:25 -0300 Subject: [PATCH 30/42] Clean up --- pkg/cheatsheet/generate.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cheatsheet/generate.go b/pkg/cheatsheet/generate.go index 19dff300..8c25da30 100644 --- a/pkg/cheatsheet/generate.go +++ b/pkg/cheatsheet/generate.go @@ -4,7 +4,7 @@ // The content of this generated file is a keybindings cheatsheet. // // To generate cheatsheet in english run: -// LANG=en go run scripts/generate_cheatsheet.go +// LANG=en go run scripts/cheatsheet/main.go generate package cheatsheet From 15ae01ebec1a06315f1dda253fb271f0ca52b88c Mon Sep 17 00:00:00 2001 From: Gustavo Andrioli Date: Mon, 10 Oct 2022 18:29:17 -0300 Subject: [PATCH 31/42] Bump lazycore --- go.mod | 5 +-- go.sum | 6 +-- pkg/cheatsheet/validate.go | 4 +- .../golang-collections/collections/LICENSE | 20 --------- .../collections/stack/stack.go | 44 ------------------- .../jesseduffield/lazycore/pkg/utils/utils.go | 35 +++++++++++++++ vendor/modules.txt | 3 +- 7 files changed, 42 insertions(+), 75 deletions(-) delete mode 100644 vendor/github.com/golang-collections/collections/LICENSE delete mode 100644 vendor/github.com/golang-collections/collections/stack/stack.go diff --git a/go.mod b/go.mod index b8a9dcd7..84730748 100644 --- a/go.mod +++ b/go.mod @@ -9,18 +9,18 @@ require ( github.com/docker/docker v20.10.15+incompatible github.com/fatih/color v1.7.0 github.com/go-errors/errors v1.4.2 - github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3 github.com/gookit/color v1.5.0 github.com/imdario/mergo v0.3.8 github.com/integrii/flaggy v1.4.0 github.com/jesseduffield/asciigraph v0.0.0-20190605104717-6d88e39309ee github.com/jesseduffield/gocui v0.3.1-0.20220417002912-bce22fd599f6 github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10 - github.com/jesseduffield/lazycore v0.0.0-20221009164442-17c8b878c316 + github.com/jesseduffield/lazycore v0.0.0-20221010211550-2c30efd18b93 github.com/jesseduffield/yaml v0.0.0-20190702115811-b900b7e08b56 github.com/mattn/go-runewidth v0.0.13 github.com/mcuadros/go-lookup v0.0.0-20171110082742-5650f26be767 github.com/mgutz/str v1.2.0 + github.com/pmezard/go-difflib v1.0.0 github.com/samber/lo v1.31.0 github.com/sirupsen/logrus v1.4.2 github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad @@ -48,7 +48,6 @@ require ( github.com/opencontainers/go-digest v1.0.0-rc1 // indirect github.com/opencontainers/image-spec v1.0.1 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.2.0 // indirect github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect golang.org/x/exp v0.0.0-20220428152302-39d4317da171 // indirect diff --git a/go.sum b/go.sum index 7daa24f7..05c3753c 100644 --- a/go.sum +++ b/go.sum @@ -33,8 +33,6 @@ github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxI github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= -github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3 h1:zN2lZNZRflqFyxVaTIU61KNKQ9C0055u9CAfpmqUvo4= -github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3/go.mod h1:nPpo7qLxd6XL3hWJG/O60sR8ZKfMCiIoNap5GvD12KU= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= @@ -54,8 +52,8 @@ github.com/jesseduffield/gocui v0.3.1-0.20220417002912-bce22fd599f6 h1:Fmay0Lz21 github.com/jesseduffield/gocui v0.3.1-0.20220417002912-bce22fd599f6/go.mod h1:znJuCDnF2Ph40YZSlBwdX/4GEofnIoWLGdT4mK5zRAU= github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10 h1:jmpr7KpX2+2GRiE91zTgfq49QvgiqB0nbmlwZ8UnOx0= github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10/go.mod h1:aA97kHeNA+sj2Hbki0pvLslmE4CbDyhBeSSTUUnOuVo= -github.com/jesseduffield/lazycore v0.0.0-20221009164442-17c8b878c316 h1:UzTg0utG+cLS94Qjb/ZFDzfMeZDFXErSbNgJOyj70EA= -github.com/jesseduffield/lazycore v0.0.0-20221009164442-17c8b878c316/go.mod h1:qxN4mHOAyeIDLP7IK7defgPClM/z1Kze8VVQiaEjzsQ= +github.com/jesseduffield/lazycore v0.0.0-20221010211550-2c30efd18b93 h1:zY7ymCjXC7fZeJVXDpiicYx6c2YFgvsdZSOdch2f7gU= +github.com/jesseduffield/lazycore v0.0.0-20221010211550-2c30efd18b93/go.mod h1:qxN4mHOAyeIDLP7IK7defgPClM/z1Kze8VVQiaEjzsQ= github.com/jesseduffield/yaml v0.0.0-20190702115811-b900b7e08b56 h1:33wSxJWU/f2TAozHYtJ8zqBxEnEVYM+22moLoiAkxvg= github.com/jesseduffield/yaml v0.0.0-20190702115811-b900b7e08b56/go.mod h1:FZJBwOhE+RXz8EVZfY+xnbCw2cVOwxlK3/aIi581z/s= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= diff --git a/pkg/cheatsheet/validate.go b/pkg/cheatsheet/validate.go index 1ad33681..033c7def 100644 --- a/pkg/cheatsheet/validate.go +++ b/pkg/cheatsheet/validate.go @@ -8,7 +8,7 @@ import ( "path/filepath" "regexp" - "github.com/jesseduffield/lazydocker/pkg/utils" + "github.com/jesseduffield/lazycore/pkg/utils" "github.com/pmezard/go-difflib/difflib" ) @@ -60,7 +60,7 @@ func Check() { } func GetKeybindingsDir() string { - return utils.GetLazydockerRootDirectory() + "/docs/keybindings" + return utils.GetLazyRootDirectory() + "/docs/keybindings" } func obtainContent(dir string) string { diff --git a/vendor/github.com/golang-collections/collections/LICENSE b/vendor/github.com/golang-collections/collections/LICENSE deleted file mode 100644 index 863a984d..00000000 --- a/vendor/github.com/golang-collections/collections/LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -Copyright (c) 2012 Caleb Doxsey - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be included -in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/vendor/github.com/golang-collections/collections/stack/stack.go b/vendor/github.com/golang-collections/collections/stack/stack.go deleted file mode 100644 index 11f472a9..00000000 --- a/vendor/github.com/golang-collections/collections/stack/stack.go +++ /dev/null @@ -1,44 +0,0 @@ -package stack - -type ( - Stack struct { - top *node - length int - } - node struct { - value interface{} - prev *node - } -) -// Create a new stack -func New() *Stack { - return &Stack{nil,0} -} -// Return the number of items in the stack -func (this *Stack) Len() int { - return this.length -} -// View the top item on the stack -func (this *Stack) Peek() interface{} { - if this.length == 0 { - return nil - } - return this.top.value -} -// Pop the top item of the stack and return it -func (this *Stack) Pop() interface{} { - if this.length == 0 { - return nil - } - - n := this.top - this.top = n.prev - this.length-- - return n.value -} -// Push a value onto the top of the stack -func (this *Stack) Push(value interface{}) { - n := &node{value,this.top} - this.top = n - this.length++ -} \ No newline at end of file diff --git a/vendor/github.com/jesseduffield/lazycore/pkg/utils/utils.go b/vendor/github.com/jesseduffield/lazycore/pkg/utils/utils.go index 9fa93376..9d0c2cc6 100644 --- a/vendor/github.com/jesseduffield/lazycore/pkg/utils/utils.go +++ b/vendor/github.com/jesseduffield/lazycore/pkg/utils/utils.go @@ -1,5 +1,11 @@ package utils +import ( + "log" + "os" + "path/filepath" +) + // Min returns the minimum of two integers func Min(x, y int) int { if x < y { @@ -8,9 +14,38 @@ func Min(x, y int) int { return y } +// Max returns the maximum of two integers func Max(x, y int) int { if x > y { return x } return y } + +// GetLazyRootDirectory finds a lazy project root directory. +// +// It's used for cheatsheet scripts and integration tests. Not to be confused with finding the +// root directory of _any_ random repo. +func GetLazyRootDirectory() string { + path, err := os.Getwd() + if err != nil { + panic(err) + } + + for { + _, err := os.Stat(filepath.Join(path, ".git")) + if err == nil { + return path + } + + if !os.IsNotExist(err) { + panic(err) + } + + path = filepath.Dir(path) + + if path == "/" { + log.Fatal("must run in lazy project folder or child folder") + } + } +} diff --git a/vendor/modules.txt b/vendor/modules.txt index a8818516..400209c5 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -102,7 +102,6 @@ github.com/go-errors/errors github.com/gogo/protobuf/proto # github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3 ## explicit -github.com/golang-collections/collections/stack # github.com/gookit/color v1.5.0 ## explicit; go 1.13 github.com/gookit/color @@ -121,7 +120,7 @@ github.com/jesseduffield/gocui # github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10 ## explicit; go 1.18 github.com/jesseduffield/kill -# github.com/jesseduffield/lazycore v0.0.0-20221009164442-17c8b878c316 +# github.com/jesseduffield/lazycore v0.0.0-20221010211550-2c30efd18b93 ## explicit; go 1.18 github.com/jesseduffield/lazycore/pkg/boxlayout github.com/jesseduffield/lazycore/pkg/utils From e8eee1dd4d4e53b8901be82f5376af41afd9ba35 Mon Sep 17 00:00:00 2001 From: Gustavo Andrioli Date: Mon, 10 Oct 2022 18:42:14 -0300 Subject: [PATCH 32/42] Update CI jobs: Add check-codebase --- .github/workflows/ci.yml | 28 ++++++++++++++++++++++++++++ vendor/modules.txt | 2 -- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 035f4b2e..74929e8b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -69,6 +69,34 @@ jobs: - name: Build darwin binary run: | GOOS=darwin go build + check-codebase: + runs-on: ubuntu-latest + env: + GOFLAGS: -mod=vendor + GOARCH: amd64 + steps: + - name: Checkout code + uses: actions/checkout@v2 + - name: Setup Go + uses: actions/setup-go@v1 + with: + go-version: 1.18.x + - name: Cache build + uses: actions/cache@v1 + with: + path: | + ~/.cache/go-build + ~/go/pkg/mod + key: ${{runner.os}}-go-${{hashFiles('**/go.sum')}}-build + restore-keys: | + ${{runner.os}}-go- + - name: Check Cheatsheet + run: | + go run scripts/cheatsheet/main.go check + - name: Check Vendor Directory + # ensure our vendor directory matches up with our go modules + run: | + go mod vendor && git diff --exit-code || (echo "Unexpected change to vendor directory. Run 'go mod vendor' locally and commit the changes" && exit 1) lint: runs-on: ubuntu-latest env: diff --git a/vendor/modules.txt b/vendor/modules.txt index 400209c5..47f06975 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -100,8 +100,6 @@ github.com/go-errors/errors # github.com/gogo/protobuf v1.3.1 ## explicit github.com/gogo/protobuf/proto -# github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3 -## explicit # github.com/gookit/color v1.5.0 ## explicit; go 1.13 github.com/gookit/color From 449e2bf5e529044c63becc0b8dbe34aaeb06cbf3 Mon Sep 17 00:00:00 2001 From: Gustavo Andrioli Date: Mon, 10 Oct 2022 18:43:18 -0300 Subject: [PATCH 33/42] Update CI jobs: Remove Check Cheatsheet from ci job --- .github/workflows/ci.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 74929e8b..7ffbda56 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,9 +35,6 @@ jobs: key: ${{runner.os}}-go-${{hashFiles('**/go.sum')}}-test restore-keys: | ${{runner.os}}-go- - - name: Check Cheatsheet - run: | - go run scripts/cheatsheet/main.go check - name: Test code run: | bash ./test.sh From bc59c6edde396fbd3a4e8769b52098744f17fa7e Mon Sep 17 00:00:00 2001 From: Gustavo Andrioli Date: Mon, 10 Oct 2022 21:22:55 -0300 Subject: [PATCH 34/42] Remove GetLazydockerRootDirectory function --- pkg/utils/utils.go | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index baf5cf5c..fe1ae3de 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -5,10 +5,7 @@ import ( "fmt" "html/template" "io" - "log" "math" - "os" - "path/filepath" "reflect" "regexp" "sort" @@ -411,31 +408,3 @@ func IsValidHexValue(v string) bool { return true } - -// GetLazydockerRootDirectory finds lazydocker root directory. -// -// It's used for our cheatsheet script and integration tests. Not to be confused with finding the -// root directory of _any_ random repo. -func GetLazydockerRootDirectory() string { - path, err := os.Getwd() - if err != nil { - panic(err) - } - - for { - _, err := os.Stat(filepath.Join(path, ".git")) - if err == nil { - return path - } - - if !os.IsNotExist(err) { - panic(err) - } - - path = filepath.Dir(path) - - if path == "/" { - log.Fatal("must run in lazydocker folder or child folder") - } - } -} From ac5c5f40c9e50a7c70cbbddd8be11f7cc865c451 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Mon, 10 Oct 2022 18:40:49 -0700 Subject: [PATCH 35/42] update sponsor link --- pkg/gui/project_panel.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/gui/project_panel.go b/pkg/gui/project_panel.go index 0ae98e9d..6edee4fa 100644 --- a/pkg/gui/project_panel.go +++ b/pkg/gui/project_panel.go @@ -118,7 +118,7 @@ func (gui *Gui) renderCredits() error { "Keybindings: https://github.com/jesseduffield/lazydocker/blob/master/docs/keybindings", "Config Options: https://github.com/jesseduffield/lazydocker/blob/master/docs/Config.md", "Raise an Issue: https://github.com/jesseduffield/lazydocker/issues", - utils.ColoredString("Buy Jesse a coffee: https://donorbox.org/lazydocker", color.FgMagenta), // caffeine ain't free + utils.ColoredString("Buy Jesse a coffee: https://github.com/sponsors/jesseduffield", color.FgMagenta), // caffeine ain't free "Here's your lazydocker config when merged in with the defaults (you can open your config by pressing 'o'):", configBuf.String(), }, "\n\n") From 8c32e623c00751ad2846e9de5529ecebd13334ba Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Mon, 10 Oct 2022 19:01:21 -0700 Subject: [PATCH 36/42] allow upping a service --- docs/Config.md | 1 + docs/keybindings/Keybindings_de.md | 1 + docs/keybindings/Keybindings_en.md | 1 + docs/keybindings/Keybindings_nl.md | 1 + docs/keybindings/Keybindings_pl.md | 1 + docs/keybindings/Keybindings_tr.md | 1 + pkg/commands/service.go | 26 ++++++++++++-------------- pkg/config/app_config.go | 4 ++++ pkg/gui/keybindings.go | 7 +++++++ pkg/gui/services_panel.go | 13 +++++++++++++ pkg/i18n/english.go | 2 ++ 11 files changed, 44 insertions(+), 14 deletions(-) diff --git a/docs/Config.md b/docs/Config.md index fecb7d3c..6e77a94a 100644 --- a/docs/Config.md +++ b/docs/Config.md @@ -43,6 +43,7 @@ commandTemplates: dockerCompose: docker-compose restartService: '{{ .DockerCompose }} restart {{ .Service.Name }}' startService: '{{ .DockerCompose }} start {{ .Service.Name }}' + upService: '{{ .DockerCompose }} up -d {{ .Service.Name }}' stopService: '{{ .DockerCompose }} stop {{ .Service.Name }}' serviceLogs: '{{ .DockerCompose }} logs --since=60m --follow {{ .Service.Name }}' viewServiceLogs: '{{ .DockerCompose }} logs --follow {{ .Service.Name }}' diff --git a/docs/keybindings/Keybindings_de.md b/docs/keybindings/Keybindings_de.md index 458cf793..b28837b1 100644 --- a/docs/keybindings/Keybindings_de.md +++ b/docs/keybindings/Keybindings_de.md @@ -35,6 +35,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct ## Dienste
+  u: up service
   d: entferne Container
   s: anhalten
   p: pause
diff --git a/docs/keybindings/Keybindings_en.md b/docs/keybindings/Keybindings_en.md
index dd0925e2..86f1b8cf 100644
--- a/docs/keybindings/Keybindings_en.md
+++ b/docs/keybindings/Keybindings_en.md
@@ -35,6 +35,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
 ## Services
 
 
+  u: up service
   d: remove containers
   s: stop
   p: pause
diff --git a/docs/keybindings/Keybindings_nl.md b/docs/keybindings/Keybindings_nl.md
index 00dc7f88..17077899 100644
--- a/docs/keybindings/Keybindings_nl.md
+++ b/docs/keybindings/Keybindings_nl.md
@@ -35,6 +35,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
 ## Diensten
 
 
+  u: up service
   d: verwijder containers
   s: stop
   p: pause
diff --git a/docs/keybindings/Keybindings_pl.md b/docs/keybindings/Keybindings_pl.md
index 174beb15..08b89c5b 100644
--- a/docs/keybindings/Keybindings_pl.md
+++ b/docs/keybindings/Keybindings_pl.md
@@ -35,6 +35,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
 ## Serwisy
 
 
+  u: up service
   d: usuń kontenery
   s: zatrzymaj
   p: pause
diff --git a/docs/keybindings/Keybindings_tr.md b/docs/keybindings/Keybindings_tr.md
index bb83faee..263db57b 100644
--- a/docs/keybindings/Keybindings_tr.md
+++ b/docs/keybindings/Keybindings_tr.md
@@ -35,6 +35,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
 ## Servisler
 
 
+  u: up service
   d: konteynerleri kaldır
   s: durdur
   p: pause
diff --git a/pkg/commands/service.go b/pkg/commands/service.go
index f45146bc..2918b339 100644
--- a/pkg/commands/service.go
+++ b/pkg/commands/service.go
@@ -38,29 +38,27 @@ func (s *Service) Remove(options types.ContainerRemoveOptions) error {
 
 // Stop stops the service's containers
 func (s *Service) Stop() error {
-	templateString := s.OSCommand.Config.UserConfig.CommandTemplates.StopService
-	command := utils.ApplyTemplate(
-		templateString,
-		s.DockerCommand.NewCommandObject(CommandObject{Service: s}),
-	)
-	return s.OSCommand.RunCommand(command)
+	return s.runCommand(s.OSCommand.Config.UserConfig.CommandTemplates.StopService)
+}
+
+// Up up's the service
+func (s *Service) Up() error {
+	return s.runCommand(s.OSCommand.Config.UserConfig.CommandTemplates.UpService)
 }
 
 // Restart restarts the service
 func (s *Service) Restart() error {
-	templateString := s.OSCommand.Config.UserConfig.CommandTemplates.RestartService
-	command := utils.ApplyTemplate(
-		templateString,
-		s.DockerCommand.NewCommandObject(CommandObject{Service: s}),
-	)
-	return s.OSCommand.RunCommand(command)
+	return s.runCommand(s.OSCommand.Config.UserConfig.CommandTemplates.RestartService)
 }
 
 // Restart starts the service
 func (s *Service) Start() error {
-	templateString := s.OSCommand.Config.UserConfig.CommandTemplates.StartService
+	return s.runCommand(s.OSCommand.Config.UserConfig.CommandTemplates.StartService)
+}
+
+func (s *Service) runCommand(templateCmdStr string) error {
 	command := utils.ApplyTemplate(
-		templateString,
+		templateCmdStr,
 		s.DockerCommand.NewCommandObject(CommandObject{Service: s}),
 	)
 	return s.OSCommand.RunCommand(command)
diff --git a/pkg/config/app_config.go b/pkg/config/app_config.go
index 78064067..a68cb444 100644
--- a/pkg/config/app_config.go
+++ b/pkg/config/app_config.go
@@ -138,6 +138,9 @@ type CommandTemplatesConfig struct {
 	// StartService is just like the above but for starting
 	StartService string `yaml:"startService,omitempty"`
 
+	// UpService ups the service (creates and starts)
+	UpService string `yaml:"upService,omitempty"`
+
 	// DockerCompose is for your docker-compose command. You may want to combine a
 	// few different docker-compose.yml files together, in which case you can set
 	// this to "docker-compose -f foo/docker-compose.yml -f
@@ -348,6 +351,7 @@ func GetDefaultConfig() UserConfig {
 			DockerCompose:            "docker-compose",
 			RestartService:           "{{ .DockerCompose }} restart {{ .Service.Name }}",
 			StartService:             "{{ .DockerCompose }} start {{ .Service.Name }}",
+			UpService:                "{{ .DockerCompose }} up -d {{ .Service.Name }}",
 			RebuildService:           "{{ .DockerCompose }} up -d --build {{ .Service.Name }}",
 			RecreateService:          "{{ .DockerCompose }} up -d --force-recreate {{ .Service.Name }}",
 			StopService:              "{{ .DockerCompose }} stop {{ .Service.Name }}",
diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go
index 05ce3498..54a6b7e7 100644
--- a/pkg/gui/keybindings.go
+++ b/pkg/gui/keybindings.go
@@ -283,6 +283,13 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
 			Handler:     gui.handleContainersOpenInBrowserCommand,
 			Description: gui.Tr.OpenInBrowser,
 		},
+		{
+			ViewName:    "services",
+			Key:         'u',
+			Modifier:    gocui.ModNone,
+			Handler:     gui.handleServiceUp,
+			Description: gui.Tr.UpService,
+		},
 		{
 			ViewName:    "services",
 			Key:         'd',
diff --git a/pkg/gui/services_panel.go b/pkg/gui/services_panel.go
index 76d0116b..67cb3613 100644
--- a/pkg/gui/services_panel.go
+++ b/pkg/gui/services_panel.go
@@ -248,6 +248,19 @@ func (gui *Gui) handleServiceStop(g *gocui.Gui, v *gocui.View) error {
 	}, nil)
 }
 
+func (gui *Gui) handleServiceUp(g *gocui.Gui, v *gocui.View) error {
+	service, err := gui.getSelectedService()
+	if err != nil {
+		return nil
+	}
+
+	if err := service.Up(); err != nil {
+		return gui.createErrorPanel(err.Error())
+	}
+
+	return nil
+}
+
 func (gui *Gui) handleServiceRestart(g *gocui.Gui, v *gocui.View) error {
 	service, err := gui.getSelectedService()
 	if err != nil {
diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go
index 9f225c4d..eed1fcb4 100644
--- a/pkg/i18n/english.go
+++ b/pkg/i18n/english.go
@@ -46,6 +46,7 @@ type TranslationSet struct {
 	RunningCustomCommandStatus  string
 	RunningBulkCommandStatus    string
 	RemoveService               string
+	UpService                   string
 	Stop                        string
 	Pause                       string
 	Restart                     string
@@ -149,6 +150,7 @@ func englishSet() TranslationSet {
 		ForceRemove:                 "force remove",
 		RemoveWithVolumes:           "remove with volumes",
 		RemoveService:               "remove containers",
+		UpService:                   "up service",
 		Stop:                        "stop",
 		Pause:                       "pause",
 		Restart:                     "restart",

From cddfb417b66b97aedc661ec7589a8490612faa2a Mon Sep 17 00:00:00 2001
From: Jesse Duffield 
Date: Mon, 10 Oct 2022 19:57:34 -0700
Subject: [PATCH 37/42] wait when upping

---
 pkg/gui/services_panel.go | 10 ++++++----
 pkg/i18n/english.go       |  2 ++
 2 files changed, 8 insertions(+), 4 deletions(-)

diff --git a/pkg/gui/services_panel.go b/pkg/gui/services_panel.go
index 67cb3613..af95e925 100644
--- a/pkg/gui/services_panel.go
+++ b/pkg/gui/services_panel.go
@@ -254,11 +254,13 @@ func (gui *Gui) handleServiceUp(g *gocui.Gui, v *gocui.View) error {
 		return nil
 	}
 
-	if err := service.Up(); err != nil {
-		return gui.createErrorPanel(err.Error())
-	}
+	return gui.WithWaitingStatus(gui.Tr.UppingStatus, func() error {
+		if err := service.Up(); err != nil {
+			return gui.createErrorPanel(err.Error())
+		}
 
-	return nil
+		return nil
+	})
 }
 
 func (gui *Gui) handleServiceRestart(g *gocui.Gui, v *gocui.View) error {
diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go
index eed1fcb4..cc11470f 100644
--- a/pkg/i18n/english.go
+++ b/pkg/i18n/english.go
@@ -41,6 +41,7 @@ type TranslationSet struct {
 	RestartingStatus            string
 	StartingStatus              string
 	StoppingStatus              string
+	UppingStatus                string
 	PausingStatus               string
 	RemovingStatus              string
 	RunningCustomCommandStatus  string
@@ -117,6 +118,7 @@ func englishSet() TranslationSet {
 		RestartingStatus:           "restarting",
 		StartingStatus:             "starting",
 		StoppingStatus:             "stopping",
+		UppingStatus:               "upping",
 		PausingStatus:              "pausing",
 		RunningCustomCommandStatus: "running custom command",
 		RunningBulkCommandStatus:   "running bulk command",

From 8b04b3ecf47568be0af03f075053c1a48c39613a Mon Sep 17 00:00:00 2001
From: Jesse Duffield 
Date: Mon, 10 Oct 2022 20:10:36 -0700
Subject: [PATCH 38/42] properly display unforwarded port

---
 pkg/commands/container.go | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/pkg/commands/container.go b/pkg/commands/container.go
index c239640d..39cece20 100644
--- a/pkg/commands/container.go
+++ b/pkg/commands/container.go
@@ -57,6 +57,10 @@ func (c *Container) GetDisplayStrings(isFocused bool) []string {
 
 func (c *Container) displayPorts() string {
 	portStrings := lo.Map(c.Container.Ports, func(port types.Port, _ int) string {
+		if port.PublicPort == 0 {
+			return fmt.Sprintf("%d\\%s", port.PrivatePort, port.Type)
+		}
+
 		// docker ps will show '0.0.0.0:80->80/tcp' but we'll show
 		// '80->80/tcp' instead to save space (unless the IP is something other than
 		// 0.0.0.0)

From e636091bba75c65fad3ee4888fae96b5782003b3 Mon Sep 17 00:00:00 2001
From: Jesse Duffield 
Date: Mon, 10 Oct 2022 20:33:56 -0700
Subject: [PATCH 39/42] allow upping and downing whole project

---
 docs/Config.md            |  5 +++-
 pkg/config/app_config.go  | 11 +++++++
 pkg/gui/keybindings.go    | 14 +++++++++
 pkg/gui/services_panel.go | 63 ++++++++++++++++++++++++++++++++++++++-
 pkg/i18n/english.go       | 16 ++++++++--
 5 files changed, 105 insertions(+), 4 deletions(-)

diff --git a/docs/Config.md b/docs/Config.md
index 6e77a94a..b1d0c4d7 100644
--- a/docs/Config.md
+++ b/docs/Config.md
@@ -42,8 +42,11 @@ logs:
 commandTemplates:
   dockerCompose: docker-compose
   restartService: '{{ .DockerCompose }} restart {{ .Service.Name }}'
-  startService: '{{ .DockerCompose }} start {{ .Service.Name }}'
+  up:  '{{ .DockerCompose }} up -d'
+  down: '{{ .DockerCompose }} down'
+  downWithVolumes: '{{ .DockerCompose }} down --volumes'
   upService:  '{{ .DockerCompose }} up -d {{ .Service.Name }}'
+  startService: '{{ .DockerCompose }} start {{ .Service.Name }}'
   stopService: '{{ .DockerCompose }} stop {{ .Service.Name }}'
   serviceLogs: '{{ .DockerCompose }} logs --since=60m --follow {{ .Service.Name }}'
   viewServiceLogs: '{{ .DockerCompose }} logs --follow {{ .Service.Name }}'
diff --git a/pkg/config/app_config.go b/pkg/config/app_config.go
index a68cb444..104c55ca 100644
--- a/pkg/config/app_config.go
+++ b/pkg/config/app_config.go
@@ -141,6 +141,14 @@ type CommandTemplatesConfig struct {
 	// UpService ups the service (creates and starts)
 	UpService string `yaml:"upService,omitempty"`
 
+	// Runs "docker-compose up -d"
+	Up string `yaml:"up,omitempty"`
+
+	// downs everything
+	Down string `yaml:"down,omitempty"`
+	// downs and removes volumes
+	DownWithVolumes string `yaml:"downWithVolumes,omitempty"`
+
 	// DockerCompose is for your docker-compose command. You may want to combine a
 	// few different docker-compose.yml files together, in which case you can set
 	// this to "docker-compose -f foo/docker-compose.yml -f
@@ -351,6 +359,9 @@ func GetDefaultConfig() UserConfig {
 			DockerCompose:            "docker-compose",
 			RestartService:           "{{ .DockerCompose }} restart {{ .Service.Name }}",
 			StartService:             "{{ .DockerCompose }} start {{ .Service.Name }}",
+			Up:                       "{{ .DockerCompose }} up -d",
+			Down:                     "{{ .DockerCompose }} down",
+			DownWithVolumes:          "{{ .DockerCompose }} down --volumes",
 			UpService:                "{{ .DockerCompose }} up -d {{ .Service.Name }}",
 			RebuildService:           "{{ .DockerCompose }} up -d --build {{ .Service.Name }}",
 			RecreateService:          "{{ .DockerCompose }} up -d --force-recreate {{ .Service.Name }}",
diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go
index 54a6b7e7..e1f12950 100644
--- a/pkg/gui/keybindings.go
+++ b/pkg/gui/keybindings.go
@@ -339,6 +339,20 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
 			Handler:     gui.handleServiceRenderLogsToMain,
 			Description: gui.Tr.ViewLogs,
 		},
+		{
+			ViewName:    "services",
+			Key:         'U',
+			Modifier:    gocui.ModNone,
+			Handler:     gui.handleProjectUp,
+			Description: gui.Tr.UpProject,
+		},
+		{
+			ViewName:    "services",
+			Key:         'D',
+			Modifier:    gocui.ModNone,
+			Handler:     gui.handleProjectDown,
+			Description: gui.Tr.DownProject,
+		},
 		{
 			ViewName:    "services",
 			Key:         '[',
diff --git a/pkg/gui/services_panel.go b/pkg/gui/services_panel.go
index af95e925..7abdd8ff 100644
--- a/pkg/gui/services_panel.go
+++ b/pkg/gui/services_panel.go
@@ -254,7 +254,7 @@ func (gui *Gui) handleServiceUp(g *gocui.Gui, v *gocui.View) error {
 		return nil
 	}
 
-	return gui.WithWaitingStatus(gui.Tr.UppingStatus, func() error {
+	return gui.WithWaitingStatus(gui.Tr.UppingServiceStatus, func() error {
 		if err := service.Up(); err != nil {
 			return gui.createErrorPanel(err.Error())
 		}
@@ -325,6 +325,67 @@ func (gui *Gui) handleServiceRenderLogsToMain(g *gocui.Gui, v *gocui.View) error
 	return gui.runSubprocess(c)
 }
 
+func (gui *Gui) handleProjectUp(g *gocui.Gui, v *gocui.View) error {
+	cmdStr := utils.ApplyTemplate(
+		gui.Config.UserConfig.CommandTemplates.Up,
+		gui.DockerCommand.NewCommandObject(commands.CommandObject{}),
+	)
+
+	return gui.WithWaitingStatus(gui.Tr.UppingAllStatus, func() error {
+		if err := gui.OSCommand.RunCommand(cmdStr); err != nil {
+			return gui.createErrorPanel(err.Error())
+		}
+		return nil
+	})
+}
+
+func (gui *Gui) handleProjectDown(g *gocui.Gui, v *gocui.View) error {
+	downCommand := utils.ApplyTemplate(
+		gui.Config.UserConfig.CommandTemplates.Down,
+		gui.DockerCommand.NewCommandObject(commands.CommandObject{}),
+	)
+
+	downWithVolumesCommand := utils.ApplyTemplate(
+		gui.Config.UserConfig.CommandTemplates.DownWithVolumes,
+		gui.DockerCommand.NewCommandObject(commands.CommandObject{}),
+	)
+
+	options := []*commandOption{
+		{
+			description: gui.Tr.Down,
+			command:     downCommand,
+			f: func() error {
+				return gui.WithWaitingStatus(gui.Tr.DowningStatus, func() error {
+					if err := gui.OSCommand.RunCommand(downCommand); err != nil {
+						return gui.createErrorPanel(err.Error())
+					}
+					return nil
+				})
+			},
+		},
+		{
+			description: gui.Tr.DownWithVolumes,
+			command:     downWithVolumesCommand,
+			f: func() error {
+				return gui.WithWaitingStatus(gui.Tr.DowningStatus, func() error {
+					if err := gui.OSCommand.RunCommand(downWithVolumesCommand); err != nil {
+						return gui.createErrorPanel(err.Error())
+					}
+					return nil
+				})
+			},
+		},
+		{
+			description: gui.Tr.Cancel,
+			f:           func() error { return nil },
+		},
+	}
+
+	handleMenuPress := func(index int) error { return options[index].f() }
+
+	return gui.createMenu("", options, len(options), handleMenuPress)
+}
+
 func (gui *Gui) handleServiceRestartMenu(g *gocui.Gui, v *gocui.View) error {
 	service, err := gui.getSelectedService()
 	if err != nil {
diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go
index cc11470f..742b2910 100644
--- a/pkg/i18n/english.go
+++ b/pkg/i18n/english.go
@@ -41,9 +41,11 @@ type TranslationSet struct {
 	RestartingStatus            string
 	StartingStatus              string
 	StoppingStatus              string
-	UppingStatus                string
+	UppingAllStatus             string
+	UppingServiceStatus         string
 	PausingStatus               string
 	RemovingStatus              string
+	DowningStatus               string
 	RunningCustomCommandStatus  string
 	RunningBulkCommandStatus    string
 	RemoveService               string
@@ -51,6 +53,8 @@ type TranslationSet struct {
 	Stop                        string
 	Pause                       string
 	Restart                     string
+	Down                        string
+	DownWithVolumes             string
 	Start                       string
 	Rebuild                     string
 	Recreate                    string
@@ -58,6 +62,8 @@ type TranslationSet struct {
 	NextContext                 string
 	Attach                      string
 	ViewLogs                    string
+	UpProject                   string
+	DownProject                 string
 	ServicesTitle               string
 	ContainersTitle             string
 	StandaloneContainersTitle   string
@@ -118,7 +124,9 @@ func englishSet() TranslationSet {
 		RestartingStatus:           "restarting",
 		StartingStatus:             "starting",
 		StoppingStatus:             "stopping",
-		UppingStatus:               "upping",
+		UppingServiceStatus:        "upping service",
+		UppingAllStatus:            "upping all",
+		DowningStatus:              "downing",
 		PausingStatus:              "pausing",
 		RunningCustomCommandStatus: "running custom command",
 		RunningBulkCommandStatus:   "running bulk command",
@@ -156,6 +164,8 @@ func englishSet() TranslationSet {
 		Stop:                        "stop",
 		Pause:                       "pause",
 		Restart:                     "restart",
+		Down:                        "down project",
+		DownWithVolumes:             "down project with volumes",
 		Start:                       "start",
 		Rebuild:                     "rebuild",
 		Recreate:                    "recreate",
@@ -163,6 +173,8 @@ func englishSet() TranslationSet {
 		NextContext:                 "next tab",
 		Attach:                      "attach",
 		ViewLogs:                    "view logs",
+		UpProject:                   "up project",
+		DownProject:                 "down project",
 		RemoveImage:                 "remove image",
 		RemoveVolume:                "remove volume",
 		RemoveWithoutPrune:          "remove without deleting untagged parents",

From dfd4db8bfa5e8de73e24ffc58400cec7db98cdc0 Mon Sep 17 00:00:00 2001
From: Jesse Duffield 
Date: Mon, 10 Oct 2022 20:34:05 -0700
Subject: [PATCH 40/42] allow jumping to top of main panel with home key

---
 pkg/gui/keybindings.go | 6 ++++++
 pkg/gui/main_panel.go  | 7 +++++++
 2 files changed, 13 insertions(+)

diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go
index e1f12950..3f8ac500 100644
--- a/pkg/gui/keybindings.go
+++ b/pkg/gui/keybindings.go
@@ -109,6 +109,12 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
 			Modifier: gocui.ModNone,
 			Handler:  gui.autoScrollMain,
 		},
+		{
+			ViewName: "",
+			Key:      gocui.KeyHome,
+			Modifier: gocui.ModNone,
+			Handler:  gui.jumpToTopMain,
+		},
 		{
 			ViewName: "",
 			Key:      'x',
diff --git a/pkg/gui/main_panel.go b/pkg/gui/main_panel.go
index e8675df4..bf985d8c 100644
--- a/pkg/gui/main_panel.go
+++ b/pkg/gui/main_panel.go
@@ -66,6 +66,13 @@ func (gui *Gui) autoScrollMain(g *gocui.Gui, v *gocui.View) error {
 	return nil
 }
 
+func (gui *Gui) jumpToTopMain(g *gocui.Gui, v *gocui.View) error {
+	gui.getMainView().Autoscroll = false
+	_ = gui.getMainView().SetOrigin(0, 0)
+	_ = gui.getMainView().SetCursor(0, 0)
+	return nil
+}
+
 func (gui *Gui) onMainTabClick(tabIndex int) error {
 	gui.Log.Warn(tabIndex)
 

From e3f01ecb8de790746417c502c1b3366010862138 Mon Sep 17 00:00:00 2001
From: Jesse Duffield 
Date: Mon, 10 Oct 2022 20:36:34 -0700
Subject: [PATCH 41/42] update cheatsheets

---
 docs/keybindings/Keybindings_de.md | 2 ++
 docs/keybindings/Keybindings_en.md | 2 ++
 docs/keybindings/Keybindings_nl.md | 2 ++
 docs/keybindings/Keybindings_pl.md | 2 ++
 docs/keybindings/Keybindings_tr.md | 2 ++
 5 files changed, 10 insertions(+)

diff --git a/docs/keybindings/Keybindings_de.md b/docs/keybindings/Keybindings_de.md
index b28837b1..898317b4 100644
--- a/docs/keybindings/Keybindings_de.md
+++ b/docs/keybindings/Keybindings_de.md
@@ -43,6 +43,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
   S: start
   a: anbinden
   m: zeige Protokolle
+  U: up project
+  D: down project
   [: vorheriges Tab
   ]: nächstes Tab
   R: zeige Neustartoptionen
diff --git a/docs/keybindings/Keybindings_en.md b/docs/keybindings/Keybindings_en.md
index 86f1b8cf..8cfab0de 100644
--- a/docs/keybindings/Keybindings_en.md
+++ b/docs/keybindings/Keybindings_en.md
@@ -43,6 +43,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
   S: start
   a: attach
   m: view logs
+  U: up project
+  D: down project
   [: previous tab
   ]: next tab
   R: view restart options
diff --git a/docs/keybindings/Keybindings_nl.md b/docs/keybindings/Keybindings_nl.md
index 17077899..f606b880 100644
--- a/docs/keybindings/Keybindings_nl.md
+++ b/docs/keybindings/Keybindings_nl.md
@@ -43,6 +43,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
   S: start
   a: verbinden
   m: bekijk logs
+  U: up project
+  D: down project
   [: vorige tab
   ]: volgende tab
   R: bekijk herstart opties
diff --git a/docs/keybindings/Keybindings_pl.md b/docs/keybindings/Keybindings_pl.md
index 08b89c5b..a8ab2a1b 100644
--- a/docs/keybindings/Keybindings_pl.md
+++ b/docs/keybindings/Keybindings_pl.md
@@ -43,6 +43,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
   S: start
   a: przyczep
   m: pokaż logi
+  U: up project
+  D: down project
   [: poprzednia zakładka
   ]: następna zakładka
   R: pokaż opcje restartu
diff --git a/docs/keybindings/Keybindings_tr.md b/docs/keybindings/Keybindings_tr.md
index 263db57b..0124bb1f 100644
--- a/docs/keybindings/Keybindings_tr.md
+++ b/docs/keybindings/Keybindings_tr.md
@@ -43,6 +43,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
   S: start
   a: bağlan/iliştir
   m: kayıt defterini görüntüle
+  U: up project
+  D: down project
   [: önceki sekme
   ]: sonraki sekme
   R: yeniden başlatma seçeneklerini görüntüle

From 15e0ecb371a5c3c82e02024da4c40556c6af0d34 Mon Sep 17 00:00:00 2001
From: Jesse Duffield 
Date: Mon, 10 Oct 2022 20:40:51 -0700
Subject: [PATCH 42/42] use confirmation panel for upping project

---
 pkg/gui/services_panel.go | 22 ++++++++++++----------
 pkg/i18n/english.go       |  6 ++++--
 2 files changed, 16 insertions(+), 12 deletions(-)

diff --git a/pkg/gui/services_panel.go b/pkg/gui/services_panel.go
index 7abdd8ff..c0361e1a 100644
--- a/pkg/gui/services_panel.go
+++ b/pkg/gui/services_panel.go
@@ -326,17 +326,19 @@ func (gui *Gui) handleServiceRenderLogsToMain(g *gocui.Gui, v *gocui.View) error
 }
 
 func (gui *Gui) handleProjectUp(g *gocui.Gui, v *gocui.View) error {
-	cmdStr := utils.ApplyTemplate(
-		gui.Config.UserConfig.CommandTemplates.Up,
-		gui.DockerCommand.NewCommandObject(commands.CommandObject{}),
-	)
+	return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.ConfirmUpProject, func(g *gocui.Gui, v *gocui.View) error {
+		cmdStr := utils.ApplyTemplate(
+			gui.Config.UserConfig.CommandTemplates.Up,
+			gui.DockerCommand.NewCommandObject(commands.CommandObject{}),
+		)
 
-	return gui.WithWaitingStatus(gui.Tr.UppingAllStatus, func() error {
-		if err := gui.OSCommand.RunCommand(cmdStr); err != nil {
-			return gui.createErrorPanel(err.Error())
-		}
-		return nil
-	})
+		return gui.WithWaitingStatus(gui.Tr.UppingProjectStatus, func() error {
+			if err := gui.OSCommand.RunCommand(cmdStr); err != nil {
+				return gui.createErrorPanel(err.Error())
+			}
+			return nil
+		})
+	}, nil)
 }
 
 func (gui *Gui) handleProjectDown(g *gocui.Gui, v *gocui.View) error {
diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go
index 742b2910..f747b5f5 100644
--- a/pkg/i18n/english.go
+++ b/pkg/i18n/english.go
@@ -17,6 +17,7 @@ type TranslationSet struct {
 	OpenConfig                                 string
 	EditConfig                                 string
 	ConfirmQuit                                string
+	ConfirmUpProject                           string
 	ErrorOccurred                              string
 	ConnectionFailed                           string
 	UnattachableContainerError                 string
@@ -41,7 +42,7 @@ type TranslationSet struct {
 	RestartingStatus            string
 	StartingStatus              string
 	StoppingStatus              string
-	UppingAllStatus             string
+	UppingProjectStatus         string
 	UppingServiceStatus         string
 	PausingStatus               string
 	RemovingStatus              string
@@ -125,7 +126,7 @@ func englishSet() TranslationSet {
 		StartingStatus:             "starting",
 		StoppingStatus:             "stopping",
 		UppingServiceStatus:        "upping service",
-		UppingAllStatus:            "upping all",
+		UppingProjectStatus:        "upping project",
 		DowningStatus:              "downing",
 		PausingStatus:              "pausing",
 		RunningCustomCommandStatus: "running custom command",
@@ -221,6 +222,7 @@ func englishSet() TranslationSet {
 		NoVolumes:    "No volumes",
 
 		ConfirmQuit:                "Are you sure you want to quit?",
+		ConfirmUpProject:           "Are you sure you want to 'up' your docker compose project?",
 		MustForceToRemoveContainer: "You cannot remove a running container unless you force it. Do you want to force it?",
 		NotEnoughSpace:             "Not enough space to render panels",
 		ConfirmPruneImages:         "Are you sure you want to prune all unused images?",