package main import ( "bytes" "errors" "fmt" "net/url" "slices" "strconv" "strings" ) var ( ErrEndOfTour = errors.New("you've hit the end of the tour") ErrStartOfTour = errors.New("you're at the start of the tour") ErrInvalidTourPos = errors.New("that's not a valid tour link") ) type Tour struct { Index int Links []*url.URL } func parseURLs(state *BrowserState, defaultScheme, str string) ([]*url.URL, error) { urls := []*url.URL{} if str == "*" { for _, link := range state.Links { urls = append(urls, state.Url.ResolveReference(link.Target)) } return urls, nil } if i := strings.IndexByte(str, '-'); i > 0 { start, e1 := strconv.Atoi(str[:i]) end, e2 := strconv.Atoi(str[i+1:]) if e1 == nil && e2 == nil && end >= start && start >= 0 && start < len(state.Links) && end < len(state.Links) { for _, link := range state.Links[start : end+1] { urls = append(urls, state.Url.ResolveReference(link.Target)) } return urls, nil } } u, _, err := parseURL(str, state, defaultScheme) if err != nil { return nil, err } return []*url.URL{u}, nil } func TourAdd(state *BrowserState, targets []string) error { newurls := []*url.URL{} for _, target := range targets { urls, err := parseURLs(state, state.DefaultScheme, target) if err != nil { return err } newurls = append(newurls, urls...) } state.CurrentTour.Links = append(state.CurrentTour.Links, newurls...) if state.CurrentTour != &state.DefaultTour { return saveTours(state.NamedTours) } state.Modal = []byte(fmt.Sprintf("Added %d urls to the tour\n", len(newurls))) return Print(state) } func TourAddNext(state *BrowserState, targets []string) error { newurls := []*url.URL{} for _, target := range targets { urls, err := parseURLs(state, state.DefaultScheme, target) if err != nil { return err } newurls = append(newurls, urls...) } state.CurrentTour.Links = slices.Insert( state.CurrentTour.Links, state.CurrentTour.Index, newurls..., ) if state.CurrentTour != &state.DefaultTour { return saveTours(state.NamedTours) } state.Modal = []byte(fmt.Sprintf("Added %d urls to go next on the tour\n", len(newurls))) return Print(state) } func TourShow(state *BrowserState) error { tour := state.CurrentTour buf := &bytes.Buffer{} for i, link := range tour.Links { mark := "" if i == tour.Index-1 { mark = "* " } if _, err := fmt.Fprintf(buf, "%s%d %s\n", mark, i, link.String()); err != nil { return err } } state.Modal = buf.Bytes() if len(state.Modal) == 0 { state.Modal = []byte("(empty)\n") } return Print(state) } func TourNext(state *BrowserState) error { tour := state.CurrentTour if tour.Index >= len(tour.Links) || len(tour.Links) == 0 { return ErrEndOfTour } page := tour.Links[tour.Index] tour.Index += 1 return Navigate(state, page, -1) } func TourPrevious(state *BrowserState) error { tour := state.CurrentTour if tour.Index <= 0 { return ErrStartOfTour } tour.Index -= 1 if tour.Index <= 0 { return ErrStartOfTour } page := tour.Links[tour.Index-1] return Navigate(state, page, -1) } func TourClear(state *BrowserState) error { state.CurrentTour.Index = 0 state.CurrentTour.Links = nil if state.CurrentTour != &state.DefaultTour { return saveTours(state.NamedTours) } state.Modal = []byte("Tour is cleared\n") return Print(state) } func TourList(state *BrowserState) error { buf := &bytes.Buffer{} mark := "" if state.CurrentTour == &state.DefaultTour { mark = "* " } if _, err := fmt.Fprintf(buf, "%s(default): %d links\n", mark, len(state.DefaultTour.Links)); err != nil { return err } for name, tour := range state.NamedTours { mark = "" if tour == state.CurrentTour { mark = "* " } if _, err := fmt.Fprintf(buf, "%s%s: %d links\n", mark, name, len(tour.Links)); err != nil { return err } } state.Modal = buf.Bytes() return Print(state) } func TourGo(state *BrowserState, pos string) error { tour := state.CurrentTour i, _ := strconv.Atoi(pos) if i < 0 || i >= len(tour.Links) { return ErrInvalidTourPos } tour.Index = i + 1 return Navigate(state, tour.Links[i], -1) } func TourSelect(state *BrowserState, name string) error { tourName, tour, err := findTour(state, name) if err != nil { return err } state.CurrentTour = tour state.Modal = []byte(fmt.Sprintf("Tour %s is now active\n", tourName)) return Print(state) } func findTour(state *BrowserState, prefix string) (string, *Tour, error) { if prefix == "" { return "(default)", &state.DefaultTour, nil } found := 0 var value *Tour var tourName string for name, tour := range state.NamedTours { if strings.HasPrefix(name, prefix) { found += 1 value = tour tourName = name } } switch found { case 0: tour := &Tour{} state.NamedTours[prefix] = tour return prefix, tour, nil case 1: return tourName, value, nil default: return "", nil, fmt.Errorf("too ambiguous - found %d matching tours", found) } }