package main import ( "bytes" "context" "fmt" "os/exec" "strings" ) func requireRofi() error { if _, err := exec.LookPath("rofi"); err != nil { return fmt.Errorf("rofi is required for interactive search") } return nil } func rofiPrompt(prompt string) (string, bool, error) { cmd := exec.Command("rofi", "-dmenu", "-p", prompt) var out bytes.Buffer cmd.Stdout = &out if err := cmd.Run(); err != nil { if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 { return "", false, nil } return "", false, fmt.Errorf("rofi prompt failed: %w", err) } selection := strings.TrimSpace(out.String()) if selection == "" { return "", false, nil } return selection, true, nil } func rofiSelect(prompt string, options []string) (string, bool, error) { cmd := exec.Command("rofi", "-dmenu", "-p", prompt) cmd.Stdin = strings.NewReader(strings.Join(options, "\n")) var out bytes.Buffer cmd.Stdout = &out if err := cmd.Run(); err != nil { if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 { return "", false, nil } return "", false, fmt.Errorf("rofi selection failed: %w", err) } selection := strings.TrimSpace(out.String()) if selection == "" { return "", false, nil } return selection, true, nil } func interactiveSearch(ctx context.Context, cache *Cache, userAgent string) (Location, error) { recent, err := cache.LoadRecentLocations() if err != nil { return Location{}, err } selection, ok, err := rofiPrompt("Search") if err != nil { return Location{}, err } if !ok { return Location{}, fmt.Errorf("interactive search cancelled") } query := selection matches := matchRecentLocations(recent, query) if len(matches) > 0 { choiceOptions := make([]string, 0, len(matches)+1) for _, loc := range matches { choiceOptions = append(choiceOptions, fmt.Sprintf("Use saved %s", loc.LabelFull())) } searchOption := fmt.Sprintf("Search for %s", query) choiceOptions = append(choiceOptions, searchOption) choice, ok, err := rofiSelect("Use saved or search", choiceOptions) if err != nil { return Location{}, err } if !ok { return Location{}, fmt.Errorf("interactive search cancelled") } if choice != searchOption { for _, loc := range matches { if choice == fmt.Sprintf("Use saved %s", loc.LabelFull()) { if err := cache.SaveLocation(loc.LabelFull(), loc); err != nil { return Location{}, err } return loc, nil } } } } fmt.Println("Searching for location...") results, err := SearchLocations(ctx, query, userAgent) if err != nil { return Location{}, err } loc, err := SelectLocationRofi(results) if err != nil { return Location{}, err } if err := cache.SaveLocation(query, loc); err != nil { return Location{}, err } return loc, nil } func SelectLocationRofi(results []Location) (Location, error) { switch len(results) { case 0: return Location{}, fmt.Errorf("no locations found") case 1: return results[0], nil } options := make([]string, 0, len(results)) for _, loc := range results { label := loc.LabelFull() if loc.Country != "" && !strings.Contains(label, loc.Country) { label = fmt.Sprintf("%s, %s", label, loc.Country) } options = append(options, label) } selection, ok, err := rofiSelect("Select", options) if err != nil { return Location{}, err } if !ok { return Location{}, fmt.Errorf("interactive selection cancelled") } for i, option := range options { if option == selection { return results[i], nil } } return Location{}, fmt.Errorf("invalid selection") } func matchRecentLocations(recent []Location, query string) []Location { query = strings.TrimSpace(strings.ToLower(query)) if query == "" { return nil } matches := make([]Location, 0, len(recent)) for _, loc := range recent { label := strings.ToLower(loc.LabelFull()) if strings.Contains(label, query) { matches = append(matches, loc) } } return matches }