weather/rofi.go

164 lines
4.0 KiB
Go

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
}
options := make([]string, 0, len(recent)+1)
for _, loc := range recent {
options = append(options, loc.LabelFull())
}
selection, ok, err := rofiSelect("Location", options)
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
}