package util import ( "context" fzf "github.com/ktr0731/go-fuzzyfinder" "golang.org/x/exp/slices" "gitea.libretechconsulting.com/rmcguire/git-project-manager/internal/cache" "gitea.libretechconsulting.com/rmcguire/git-project-manager/internal/remotes/projects" ) type FzfProjectOpts struct { Ctx context.Context Search string MustHaveAlias bool Remotes []string } // This will try to find a project by alias if a search term // is given, otherwise will fuzzy find by project func (u *Utils) FzfFindProject(opts *FzfProjectOpts) *projects.Project { var project *projects.Project if opts.Search != "" { project = u.FzfSearchProjectAliases(opts) } else { var err error project, err = u.FzfProject(opts) if project == nil || err != nil { return nil } } return project } // If . is given as a project, will open project from the // current working directory. Otherwise, will attempt to fuzzy-find // a project given a search term if provided func (u *Utils) FzfCwdOrSearchProjectAliases(opts *FzfProjectOpts) *projects.Project { var project *projects.Project if opts.Search == "." { project, _ = u.Cache().GetProjectFromCwd() } else { project = u.FzfSearchProjectAliases(opts) } return project } // This will fuzzy search only aliases, preferring an exact // match if one is given func (u *Utils) FzfSearchProjectAliases(opts *FzfProjectOpts) *projects.Project { var project *projects.Project var alias *cache.ProjectAlias if alias = u.Cache().GetAliasByName(opts.Search, opts.Remotes...); alias != nil { project = u.Cache().GetProjectByAlias(alias) u.Logger().Info("Perfect alias match... flawless") } else { // Get fuzzy if we don't have an exact match aliases := u.Cache().FuzzyFindAlias(opts.Search) if len(aliases) > 1 { // If multiple aliases were found, switch over to project // by alias mode with merging // alias = fzfAliasFromAliases(rootCmd.Context(), aliases) project, _ = u.FzfProjectFromAliases(opts, aliases) } else if len(aliases) == 1 { alias = aliases[0] project = u.Cache().GetProjectByAlias(alias) } } return project } // Given a list of aliases, will fuzzy-find and return // a single one. Replaced by fzfProjectFromAliases in fzfSearchProjectAliases // as merging is preferred, but can be used if it's ever desirable to // return a single alias from all aliases func (u *Utils) FzfAliasFromAliases(opts *FzfProjectOpts, aliases []*cache.ProjectAlias) *cache.ProjectAlias { var alias *cache.ProjectAlias i, err := fzf.Find( aliases, func(i int) string { return aliases[i].Alias + " -> " + u.Cache().GetProjectByAlias(aliases[i]).PathWithNamespace }, fzf.WithContext(opts.Ctx), fzf.WithHeader("Choose an Alias"), ) if err != nil { u.Logger().Error("Failed to fzf alias slice", u.Logger().Args("error", err)) } else { alias = aliases[i] } return alias } // Given a list of aliases, merge them together and use the resulting // list of projects to return a project func (u *Utils) FzfProjectFromAliases(opts *FzfProjectOpts, aliases []*cache.ProjectAlias) ( *projects.Project, error, ) { mergedProjects := u.projectsFromAliases(aliases) if len(mergedProjects) == 1 { return mergedProjects[0], nil } return u.FzfProjectFromProjects(opts, mergedProjects) } func (u *Utils) projectsFromAliases(aliases []*cache.ProjectAlias) []*projects.Project { projects := make([]*projects.Project, 0, len(aliases)) for _, a := range aliases { project := u.Cache().GetProjectByAlias(a) if project != nil && !slices.Contains(projects, project) { projects = append(projects, project) } } return slices.Clip(projects) } // If opts.MustHaveAlias, will only allow selection of projects // that have at least one alias defined func (u *Utils) FzfProject(opts *FzfProjectOpts) (*projects.Project, error) { var searchableProjects []*projects.Project if opts.MustHaveAlias { searchableProjects = u.Cache().GetProjectsWithAliases() } else { searchableProjects = u.Cache().Projects } // Filter out unwanted remotes if provided searchableProjects = u.FilterProjectsWithRemotes(searchableProjects, opts.Remotes...) return u.FzfProjectFromProjects(opts, searchableProjects) } // Takes a list of projects and performs a fuzzyfind func (u *Utils) FzfProjectFromProjects(opts *FzfProjectOpts, projects []*projects.Project) ( *projects.Project, error, ) { i, err := fzf.Find(projects, func(i int) string { // Display the project along with its aliases return u.Cache().GetProjectStringWithAliases(projects[i]) }, fzf.WithPreviewWindow( func(i, width, height int) string { return u.Cache().ProjectString(projects[i]) }, ), fzf.WithContext(opts.Ctx), fzf.WithHeader("Fuzzy find yourself a project"), ) if err != nil || i < 0 { return nil, err } return projects[i], nil } func (u *Utils) FzfPreviewWindow(i, _, _ int) string { p := u.Cache().Projects[i] return u.Cache().ProjectString(p) } func (u *Utils) FilterProjectsWithRemotes(gitProjects []*projects.Project, remotes ...string) []*projects.Project { filteredProjects := make([]*projects.Project, 0, len(gitProjects)) if len(remotes) > 0 { for _, p := range gitProjects { if slices.Contains(remotes, p.Remote) { filteredProjects = append(filteredProjects, p) } } } else { filteredProjects = gitProjects } return filteredProjects } // Nearly useless function that simply returns either an // empty string, or a string from the first arg if one is provided func (u *Utils) SearchStringFromArgs(args []string) string { var term string if len(args) > 0 { term = args[0] } return term }