about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--internal/components/combined.templ38
-rw-r--r--internal/components/data.go3
-rw-r--r--internal/components/page.templ26
-rw-r--r--internal/components/results.templ19
-rw-r--r--internal/config/importer-type.go7
-rw-r--r--internal/index/search.go69
-rw-r--r--internal/server/mux.go50
7 files changed, 158 insertions, 54 deletions
diff --git a/internal/components/combined.templ b/internal/components/combined.templ
new file mode 100644
index 0000000..eeacdb5
--- /dev/null
+++ b/internal/components/combined.templ
@@ -0,0 +1,38 @@
+package components
+
+import (
+	"go.alanpearce.eu/searchix/internal/index"
+	"go.alanpearce.eu/searchix/internal/nix"
+)
+
+templ Combined(result *index.Result) {
+	<table>
+		<thead>
+			<tr>
+				<th scope="col">Attribute</th>
+				<th scope="col">Description</th>
+			</tr>
+		</thead>
+		<tbody>
+			for _, hit := range result.Hits {
+				<tr>
+					<td>
+						@openCombinedDialogLink(nix.GetKey(hit.Data))
+					</td>
+					<td>
+						switch hit.Data.(type) {
+							case nix.Option:
+								if o := convertMatch[nix.Option](hit.Data); o != nil {
+									@markdown(firstSentence(o.Description))
+								}
+							case nix.Package:
+								if o := convertMatch[nix.Package](hit.Data); o != nil {
+									{ firstSentence(o.Description) }
+								}
+						}
+					</td>
+				</tr>
+			}
+		</tbody>
+	</table>
+}
diff --git a/internal/components/data.go b/internal/components/data.go
index 8f4fe62..601102d 100644
--- a/internal/components/data.go
+++ b/internal/components/data.go
@@ -11,9 +11,8 @@ import (
 
 type TemplateData struct {
 	Sources       []*config.Source
-	Source        config.Source
+	Source        *config.Source
 	Query         string
-	Results       bool
 	SourceResult  *bleve.SearchResult
 	ExtraHeadHTML string
 	Code          int
diff --git a/internal/components/page.templ b/internal/components/page.templ
index da0322e..ade677f 100644
--- a/internal/components/page.templ
+++ b/internal/components/page.templ
@@ -18,11 +18,17 @@ templ Page(tdata TemplateData) {
 				<link href={ sheet.URL } rel="stylesheet" integrity={ "sha256-" + sheet.Base64SHA256 }/>
 			}
 			@Unsafe(tdata.ExtraHeadHTML)
+			<link
+				rel="search"
+				type="application/opensearchdescription+xml"
+				title={ "Searchix " + sourceNameAndType(nil) }
+				href={ string(joinPath("/all", "opensearch.xml")) }
+			/>
 			for _, source := range tdata.Sources {
 				<link
 					rel="search"
 					type="application/opensearchdescription+xml"
-					title={ "Searchix " + sourceNameAndType(*source) }
+					title={ "Searchix " + sourceNameAndType(source) }
 					href={ string(joinPath("/", source.Importer.String(), source.Key, "opensearch.xml")) }
 				/>
 			}
@@ -31,9 +37,19 @@ templ Page(tdata TemplateData) {
 			<header>
 				<nav>
 					<h1><a href="/">Searchix</a></h1>
+					<a
+						if tdata.Source == nil {
+							if tdata.SourceResult != nil || tdata.Query != "" {
+								class="current"
+							}
+							href="/all/search"
+						} else {
+							href={ joinPathQuery("/all/search", tdata.Query) }
+						}
+					>All</a>
 					for _, source := range tdata.Sources {
 						<a
-							if tdata.Source.Name == source.Name {
+							if tdata.Source != nil && tdata.Source.Name == source.Name {
 								class="current"
 								href={ joinPath("/", source.Importer.String(), source.Key, "search") }
 							} else {
@@ -65,7 +81,11 @@ func Unsafe(html string) templ.Component {
 	})
 }
 
-func sourceNameAndType(source config.Source) string {
+func sourceNameAndType(source *config.Source) string {
+	if source == nil {
+		return "Combined"
+	}
+
 	switch source.Importer {
 	case config.Options:
 		return source.Name + " " + source.Importer.String()
diff --git a/internal/components/results.templ b/internal/components/results.templ
index 6d85e16..226b71e 100644
--- a/internal/components/results.templ
+++ b/internal/components/results.templ
@@ -3,6 +3,7 @@ package components
 import (
 	"strconv"
 	"go.alanpearce.eu/searchix/internal/nix"
+	"go.alanpearce.eu/searchix/internal/config"
 )
 
 func convertMatch[I nix.Importable](m nix.Importable) *I {
@@ -16,11 +17,15 @@ func convertMatch[I nix.Importable](m nix.Importable) *I {
 templ Results(r ResultData) {
 	if r.Query != "" {
 		if r.Results != nil && r.Results.Total > 0 {
-			switch r.Results.Hits[0].Data.(type) {
-				case nix.Option:
-					@Options(r.Results)
-				case nix.Package:
-					@Packages(r.Results)
+			if r.Source != nil {
+				switch r.Source.Importer {
+					case config.Options:
+						@Options(r.Results)
+					case config.Packages:
+						@Packages(r.Results)
+				}
+			} else {
+				@Combined(r.Results)
 			}
 			<footer aria-label="pagination">
 				<nav id="pagination">
@@ -50,3 +55,7 @@ templ ResultsPage(r ResultData) {
 templ openDialogLink(attr string) {
 	<a class="open-dialog" href={ templ.SafeURL(attr) }>{ attr }</a>
 }
+
+templ openCombinedDialogLink(attr string) {
+	<a class="open-dialog" href={ templ.SafeURL("/" + attr) }>{ attr }</a>
+}
diff --git a/internal/config/importer-type.go b/internal/config/importer-type.go
index 9bd50b6..6003d7d 100644
--- a/internal/config/importer-type.go
+++ b/internal/config/importer-type.go
@@ -9,13 +9,16 @@ import (
 type ImporterType int
 
 const (
-	UnknownType = iota
+	All = iota - 1
+	UnknownType
 	Packages
 	Options
 )
 
 func (i ImporterType) String() string {
 	switch i {
+	case All:
+		return "combined"
 	case Packages:
 		return "packages"
 	case Options:
@@ -27,6 +30,8 @@ func (i ImporterType) String() string {
 
 func (i ImporterType) Singular() string {
 	switch i {
+	case All:
+		return "combined"
 	case Packages:
 		return "package"
 	case Options:
diff --git a/internal/index/search.go b/internal/index/search.go
index 03a8e60..c5b8c6b 100644
--- a/internal/index/search.go
+++ b/internal/index/search.go
@@ -19,7 +19,7 @@ import (
 const ResultsPerPage = 20
 
 type DocumentMatch struct {
-	search.DocumentMatch
+	*search.DocumentMatch
 	Data nix.Importable
 }
 
@@ -53,9 +53,18 @@ func (index *ReadIndex) GetEnabledSources() ([]string, error) {
 	return enabledSources, nil
 }
 
-func (index *ReadIndex) GetSource(ctx context.Context, name string) (*bleve.SearchResult, error) {
-	query := bleve.NewTermQuery(name)
-	query.SetField("Source")
+func (index *ReadIndex) GetSource(
+	ctx context.Context,
+	source *config.Source,
+) (*bleve.SearchResult, error) {
+	var query query.Query
+	if source == nil {
+		query = bleve.NewMatchAllQuery()
+	} else {
+		tq := bleve.NewTermQuery(source.Name)
+		tq.SetField("Source")
+		query = tq
+	}
 	search := bleve.NewSearchRequest(query)
 
 	result, err := index.index.SearchInContext(ctx, search)
@@ -68,7 +77,7 @@ func (index *ReadIndex) GetSource(ctx context.Context, name string) (*bleve.Sear
 			return nil, errors.WithMessagef(
 				err,
 				"failed to execute search to find source %s in index",
-				name,
+				source,
 			)
 		}
 	}
@@ -89,7 +98,7 @@ func (index *ReadIndex) search(
 	ctx context.Context,
 	request *bleve.SearchRequest,
 ) (*Result, error) {
-	request.Fields = []string{"_data"}
+	request.Fields = []string{"_data", "Source"}
 
 	bleveResult, err := index.index.SearchInContext(ctx, request)
 	select {
@@ -103,6 +112,7 @@ func (index *ReadIndex) search(
 		results := make([]DocumentMatch, min(ResultsPerPage, bleveResult.Total))
 		var buf bytes.Buffer
 		for i, result := range bleveResult.Hits {
+			results[i].DocumentMatch = bleveResult.Hits[i]
 			_, err = buf.WriteString(result.Fields["_data"].(string))
 			if err != nil {
 				return nil, errors.WithMessage(err, "error fetching result data")
@@ -133,37 +143,28 @@ func (index *ReadIndex) Search(
 	userQuery := bleve.NewMatchQuery(keyword)
 	userQuery.Analyzer = "option_name"
 
-	query.AddMust(
-		setField(bleve.NewTermQuery(source.Key), "Source"),
-		userQuery,
-	)
-
-	switch source.Importer {
-	case config.Packages:
-		// ...and boost it if it matches any of these
-		query.AddShould(
-			setField(bleve.NewMatchQuery(keyword), "MainProgram"),
-			setField(bleve.NewMatchQuery(keyword), "Name"),
-			setField(bleve.NewMatchQuery(keyword), "Attribute"),
-		)
-	case config.Options:
-		query.AddShould(
-			setField(bleve.NewMatchQuery(keyword), "Loc"),
-			setField(bleve.NewMatchQuery(keyword), "Name"),
-		)
-		nameLiteralQuery := bleve.NewMatchQuery(keyword)
-		nameLiteralQuery.SetField("Name")
-		nameLiteralQuery.Analyzer = standard.Name
-		query.AddShould(
-			nameLiteralQuery,
-		)
-	default:
-		return nil, errors.Errorf(
-			"unsupported source type for search: %s",
-			source.Importer.String(),
+	if source != nil {
+		query.AddMust(
+			setField(bleve.NewTermQuery(source.Key), "Source"),
+			userQuery,
 		)
 	}
 
+	// ...and boost it if it matches any of these
+	query.AddShould(
+		setField(bleve.NewMatchQuery(keyword), "MainProgram"),
+		setField(bleve.NewMatchQuery(keyword), "Name"),
+		setField(bleve.NewMatchQuery(keyword), "Attribute"),
+	)
+	query.AddShould(
+		setField(bleve.NewMatchQuery(keyword), "Loc"),
+		setField(bleve.NewMatchQuery(keyword), "Name"),
+	)
+	nameLiteralQuery := bleve.NewMatchQuery(keyword)
+	nameLiteralQuery.SetField("Name")
+	nameLiteralQuery.Analyzer = standard.Name
+	query.AddShould(nameLiteralQuery)
+
 	search := bleve.NewSearchRequest(query)
 	search.Size = ResultsPerPage
 
diff --git a/internal/server/mux.go b/internal/server/mux.go
index d1a0e38..f98e03e 100644
--- a/internal/server/mux.go
+++ b/internal/server/mux.go
@@ -89,11 +89,14 @@ func NewMux(
 	createSearchHandler := func(importerType config.ImporterType) func(http.ResponseWriter, *http.Request) {
 		return func(w http.ResponseWriter, r *http.Request) {
 			var err error
-			source := cfg.Importer.Sources[r.PathValue("source")]
-			if source == nil || importerType != source.Importer {
-				errorHandler(w, r, http.StatusText(http.StatusNotFound), http.StatusNotFound)
+			var source *config.Source
+			if importerType != config.All {
+				source = cfg.Importer.Sources[r.PathValue("source")]
+				if source == nil || importerType != source.Importer {
+					errorHandler(w, r, http.StatusText(http.StatusNotFound), http.StatusNotFound)
 
-				return
+					return
+				}
 			}
 
 			ctx, cancel := context.WithTimeout(r.Context(), searchTimeout)
@@ -127,7 +130,7 @@ func NewMux(
 				tdata := components.ResultData{
 					TemplateData: components.TemplateData{
 						ExtraHeadHTML: cfg.Web.ExtraHeadHTML,
-						Source:        *source,
+						Source:        source,
 						Sources:       sources,
 						Assets:        frontend.Assets,
 						Query:         qs,
@@ -181,19 +184,18 @@ func NewMux(
 					errorHandler(w, r, err.Error(), http.StatusInternalServerError)
 				}
 			} else {
-				sourceResult, err := index.GetSource(ctx, source.Key)
+				sourceResult, err := index.GetSource(ctx, source)
 				if err != nil {
 					errorHandler(w, r, err.Error(), http.StatusInternalServerError)
 
 					return
 				}
-
 				w.Header().Add("Cache-Control", "max-age=14400")
 				err = components.SearchPage(
 					components.TemplateData{
 						ExtraHeadHTML: cfg.Web.ExtraHeadHTML,
 						Sources:       sources,
-						Source:        *source,
+						Source:        source,
 						SourceResult:  sourceResult,
 						Assets:        frontend.Assets,
 					},
@@ -208,6 +210,7 @@ func NewMux(
 		}
 	}
 
+	mux.HandleFunc("/all/search", createSearchHandler(config.All))
 	mux.HandleFunc("/options/{source}/search", createSearchHandler(config.Options))
 	mux.HandleFunc("/packages/{source}/search", createSearchHandler(config.Packages))
 
@@ -245,7 +248,7 @@ func NewMux(
 			tdata := components.DocumentData{
 				TemplateData: components.TemplateData{
 					ExtraHeadHTML: cfg.Web.ExtraHeadHTML,
-					Source:        *source,
+					Source:        source,
 					Sources:       sources,
 					Assets:        frontend.Assets,
 				},
@@ -265,6 +268,8 @@ func NewMux(
 	}
 	mux.HandleFunc("/options/{source}/{id}", createSourceIDHandler(config.Options))
 	mux.HandleFunc("/packages/{source}/{id}", createSourceIDHandler(config.Packages))
+	mux.HandleFunc("/option/{source}/{id}", createSourceIDHandler(config.Options))
+	mux.HandleFunc("/package/{source}/{id}", createSourceIDHandler(config.Packages))
 
 	createOpenSearchXMLHandler := func(importerType config.ImporterType) func(http.ResponseWriter, *http.Request) {
 		return func(w http.ResponseWriter, r *http.Request) {
@@ -312,6 +317,33 @@ func NewMux(
 
 	mux.HandleFunc("/options/{source}/opensearch.xml", createOpenSearchXMLHandler(config.Options))
 	mux.HandleFunc("/packages/{source}/opensearch.xml", createOpenSearchXMLHandler(config.Packages))
+	mux.HandleFunc("/all/opensearch.xml", func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Add("Cache-Control", "max-age=604800")
+		w.Header().Set("Content-Type", "application/opensearchdescription+xml")
+		osd := &opensearch.Description{
+			ShortName:   "Searchix Combined",
+			LongName:    "Search nix options and packages with Searchix",
+			Description: "Search nix options and packages with Searchix",
+			SearchForm:  cfg.Web.BaseURL.JoinPath("all/search"),
+			URL: opensearch.URL{
+				Method: "get",
+				Type:   "text/html",
+				Template: cfg.Web.BaseURL.JoinPath("all/search").
+					AddRawQuery("query", "{searchTerms}"),
+			},
+		}
+		enc := xml.NewEncoder(w)
+		enc.Indent("", "    ")
+		err := enc.Encode(osd)
+		if err != nil {
+			// no errorHandler; HTML does not make sense here
+			http.Error(
+				w,
+				fmt.Sprintf("OpenSearch XML encoding error: %v", err),
+				http.StatusInternalServerError,
+			)
+		}
+	})
 
 	mux.HandleFunc("/static/", func(w http.ResponseWriter, r *http.Request) {
 		asset, found := frontend.Assets.ByPath[r.URL.Path]