From e8a1a4ffc0ab0e10ee0ad43042dd5a2fd2438353 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Tue, 4 Jan 2022 10:46:14 +1100 Subject: add cheatsheet check script --- pkg/cheatsheet/check.go | 79 ++++++++++++++ pkg/cheatsheet/generate.go | 265 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 344 insertions(+) create mode 100644 pkg/cheatsheet/check.go create mode 100644 pkg/cheatsheet/generate.go (limited to 'pkg') diff --git a/pkg/cheatsheet/check.go b/pkg/cheatsheet/check.go new file mode 100644 index 000000000..03f65d910 --- /dev/null +++ b/pkg/cheatsheet/check.go @@ -0,0 +1,79 @@ +package cheatsheet + +import ( + "fmt" + "io/fs" + "io/ioutil" + "log" + "os" + "path/filepath" + "regexp" + + "github.com/pmezard/go-difflib/difflib" +) + +func Check() { + dir := GetDir() + tmpDir := filepath.Join(os.TempDir(), "lazygit_cheatsheet") + err := os.RemoveAll(tmpDir) + if err != nil { + log.Fatalf("Error occured while checking if cheatsheets are up to date: %v", err) + } + err = os.Mkdir(tmpDir, 0700) + if err != nil { + log.Fatalf("Error occured while checking if cheatsheets are up to date: %v", err) + } + + generateAtDir(tmpDir) + defer os.RemoveAll(tmpDir) + + actualContent := obtainContent(dir) + expectedContent := obtainContent(tmpDir) + + if expectedContent == "" { + log.Fatal("empty expected content") + } + + if actualContent != expectedContent { + err := difflib.WriteUnifiedDiff(os.Stdout, difflib.UnifiedDiff{ + A: difflib.SplitLines(expectedContent), + B: difflib.SplitLines(actualContent), + FromFile: "Expected", + FromDate: "", + ToFile: "Actual", + ToDate: "", + Context: 1, + }) + if err != nil { + log.Fatalf("Error occured 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\n", CommandToRun()) + os.Exit(1) + } + + fmt.Println("\nCheatsheets are up to date") +} + +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 := ioutil.ReadFile(path) + if err != nil { + log.Fatalf("Error occured 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 occured while checking if cheatsheets are up to date: %v", err) + } + + return content +} diff --git a/pkg/cheatsheet/generate.go b/pkg/cheatsheet/generate.go new file mode 100644 index 000000000..513049f23 --- /dev/null +++ b/pkg/cheatsheet/generate.go @@ -0,0 +1,265 @@ +// This "script" generates a file called Keybindings_{{.LANG}}.md +// in current working directory. +// +// The content of this generated file is a keybindings cheatsheet. +// +// To generate cheatsheet in english run: +// go run scripts/generate_cheatsheet.go + +package cheatsheet + +import ( + "fmt" + "log" + "os" + "sort" + + "github.com/jesseduffield/lazygit/pkg/app" + "github.com/jesseduffield/lazygit/pkg/config" + "github.com/jesseduffield/lazygit/pkg/gui" + "github.com/jesseduffield/lazygit/pkg/i18n" + "github.com/jesseduffield/lazygit/pkg/integration" +) + +type bindingSection struct { + title string + bindings []*gui.Binding +} + +func CommandToRun() string { + return "go run scripts/cheatsheet/main.go generate" +} + +func GetDir() string { + return integration.GetRootDirectory() + "/docs/keybindings" +} + +func generateAtDir(cheatsheetDir string) { + os.Setenv("LANG", "en") + + translationSetsByLang := i18n.GetTranslationSets() + mConfig := config.NewDummyAppConfig() + + for lang := range translationSetsByLang { + os.Setenv("LC_ALL", lang) + mApp, _ := app.NewApp(mConfig, "") + path := cheatsheetDir + "/Keybindings_" + lang + ".md" + file, err := os.Create(path) + if err != nil { + panic(err) + } + + bindingSections := getBindingSections(mApp) + content := formatSections(mApp.Tr, 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) + } +} + +func Generate() { + generateAtDir(GetDir()) +} + +func writeString(file *os.File, str string) { + _, err := file.WriteString(str) + if err != nil { + log.Fatal(err) + } +} + +func localisedTitle(mApp *app.App, str string) string { + tr := mApp.Tr + + contextTitleMap := map[string]string{ + "global": tr.GlobalTitle, + "navigation": tr.NavigationTitle, + "branches": tr.BranchesTitle, + "localBranches": tr.LocalBranchesTitle, + "files": tr.FilesTitle, + "status": tr.StatusTitle, + "submodules": tr.SubmodulesTitle, + "subCommits": tr.SubCommitsTitle, + "remoteBranches": tr.RemoteBranchesTitle, + "remotes": tr.RemotesTitle, + "reflogCommits": tr.ReflogCommitsTitle, + "tags": tr.TagsTitle, + "commitFiles": tr.CommitFilesTitle, + "commitMessage": tr.CommitMessageTitle, + "commits": tr.CommitsTitle, + "confirmation": tr.ConfirmationTitle, + "credentials": tr.CredentialsTitle, + "information": tr.InformationTitle, + "main": tr.MainTitle, + "patchBuilding": tr.PatchBuildingTitle, + "merging": tr.MergingTitle, + "normal": tr.NormalTitle, + "staging": tr.StagingTitle, + "menu": tr.MenuTitle, + "search": tr.SearchTitle, + "secondary": tr.SecondaryTitle, + "stash": tr.StashTitle, + "suggestions": tr.SuggestionsCheatsheetTitle, + "extras": tr.ExtrasTitle, + } + + title, ok := contextTitleMap[str] + if !ok { + panic(fmt.Sprintf("title not found for %s", str)) + } + + return title +} + +func formatTitle(title string) string { + return fmt.Sprintf("\n## %s\n\n", title) +} + +func formatBinding(binding *gui.Binding) string { + if binding.Alternative != "" { + return fmt.Sprintf(" %s: %s (%s)\n", gui.GetKeyDisplay(binding.Key), binding.Description, binding.Alternative) + } + return fmt.Sprintf(" %s: %s\n", gui.GetKeyDisplay(binding.Key), binding.Description) +} + +func getBindingSections(mApp *app.App) []*bindingSection { + bindingSections := []*bindingSection{} + + bindings := mApp.Gui.GetInitialKeybindings() + + type contextAndViewType struct { + subtitle string + title string + } + + contextAndViewBindingMap := map[contextAndViewType][]*gui.Binding{} + +outer: + for _, binding := range bindings { + if binding.Tag == "navigation" { + key := contextAndViewType{subtitle: "", title: "navigation"} + existing := contextAndViewBindingMap[key] + if existing == nil { + contextAndViewBindingMap[key] = []*gui.Binding{binding} + } else { + for _, navBinding := range contextAndViewBindingMap[key] { + if navBinding.Description == binding.Description { + continue outer + } + } + contextAndViewBindingMap[key] = append(contextAndViewBindingMap[key], binding) + } + + continue outer + } + + contexts := []string{} + if len(binding.Contexts) == 0 { + contexts = append(contexts, "") + } else { + contexts = append(contexts, binding.Contexts...) + } + + for _, context := range contexts { + key := contextAndViewType{subtitle: context, title: binding.ViewName} + existing := contextAndViewBindingMap[key] + if existing == nil { + contextAndViewBindingMap[key] = []*gui.Binding{binding} + } else { + contextAndViewBindingMap[key] = append(contextAndViewBindingMap[key], binding) + } + } + } + + type groupedBindingsType struct { + contextAndView contextAndViewType + bindings []*gui.Binding + } + + groupedBindings := make([]groupedBindingsType, len(contextAndViewBindingMap)) + + for contextAndView, contextBindings := range contextAndViewBindingMap { + groupedBindings = append(groupedBindings, groupedBindingsType{contextAndView: contextAndView, bindings: contextBindings}) + } + + sort.Slice(groupedBindings, func(i, j int) bool { + first := groupedBindings[i].contextAndView + second := groupedBindings[j].contextAndView + if first.title == "" { + return true + } + if second.title == "" { + return false + } + if first.title == "navigation" { + return true + } + if second.title == "navigation" { + return false + } + return first.title < second.title || (first.title == second.title && first.subtitle < second.subtitle) + }) + + for _, group := range groupedBindings { + contextAndView := group.contextAndView + contextBindings := group.bindings + mApp.Log.Info("viewname: " + contextAndView.title + ", context: " + contextAndView.subtitle) + viewName := contextAndView.title + if viewName == "" { + viewName = "global" + } + translatedView := localisedTitle(mApp, viewName) + var title string + if contextAndView.subtitle == "" { + addendum := " " + mApp.Tr.Panel + if viewName == "global" || viewName == "navigation" { + addendum = "" + } + title = fmt.Sprintf("%s%s", translatedView, addendum) + } else { + translatedContextName := localisedTitle(mApp, contextAndView.subtitle) + title = fmt.Sprintf("%s %s (%s)", translatedView, mApp.Tr.Panel, translatedContextName) + } + + for _, binding := range contextBindings { + bindingSections = addBinding(title, bindingSections, binding) + } + } + + return bindingSections +} + +func addBinding(title string, bindingSections []*bindingSection, binding *gui.Binding) []*bindingSection { + if binding.Description == "" && binding.Alternative == "" { + return bindingSections + } + + for _, section := range bindingSections { + if title == section.title { + section.bindings = append(section.bindings, binding) + return bindingSections + } + } + + section := &bindingSection{ + title: title, + bindings: []*gui.Binding{binding}, + } + + return append(bindingSections, section) +} + +func formatSections(tr *i18n.TranslationSet, bindingSections []*bindingSection) string { + content := fmt.Sprintf("# Lazygit %s\n", tr.Keybindings) + + for _, section := range bindingSections { + content += formatTitle(section.title) + content += "
\n"
+		for _, binding := range section.bindings {
+			content += formatBinding(binding)
+		}
+		content += "
\n" + } + + return content +} -- cgit v1.2.3