diff --git a/main.go b/main.go
index 836b1ab..6c76658 100644
--- a/main.go
+++ b/main.go
@@ -1,4 +1,3 @@
-// main.go
package main
import (
@@ -16,6 +15,7 @@ import (
"net/http"
"net/url"
"os"
+ "runtime"
"strconv"
"strings"
"sync"
@@ -56,6 +56,9 @@ type BookmarkEntry struct {
var bookmarkKey []byte
var bookmarkingEnabled bool
var disableReverse bool
+var chunkedMode bool
+var chunkSize = 8
+var chunkWorkers = 4
const maxBookmarks = 30
const maxItemLen = 256
@@ -85,6 +88,40 @@ func init() {
default:
disableReverse = false
}
+
+ // CHUNK enables chunked/threaded rendering of result cards.
+ // Examples:
+ // CHUNK=0/false/no/off -> disabled
+ // CHUNK=1/true/yes/on -> enabled with default chunk size
+ // CHUNK=12 -> enabled with 12-item batches
+ if raw := strings.TrimSpace(os.Getenv("CHUNK")); raw != "" {
+ switch strings.ToLower(raw) {
+ case "0", "false", "no", "off":
+ chunkedMode = false
+ default:
+ chunkedMode = true
+ if n, err := strconv.Atoi(raw); err == nil && n > 0 {
+ chunkSize = n
+ }
+ }
+ }
+ if chunkSize < 4 {
+ chunkSize = 4
+ }
+ if chunkSize > 16 {
+ chunkSize = 16
+ }
+ cpus := runtime.GOMAXPROCS(0)
+ if cpus < 1 {
+ cpus = 1
+ }
+ if cpus > 4 {
+ cpus = 4
+ }
+ chunkWorkers = cpus
+ if chunkedMode {
+ log.Printf("Chunked mode enabled: chunkSize=%d workers=%d", chunkSize, chunkWorkers)
+ }
}
// ---------- encryption helpers (AES-GCM) ----------
@@ -344,7 +381,6 @@ button[type="submit"],.btn-save{background:linear-gradient(90deg,var(--accent),#
}
`
-
// ---------- handlers ----------
func styleHandler(w http.ResponseWriter, r *http.Request) {
@@ -395,6 +431,99 @@ func settingsPostHandler(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, next, http.StatusSeeOther)
}
+func renderCardHTML(q, next, u string) string {
+ esc := url.QueryEscape(u)
+ b64 := base64.StdEncoding.EncodeToString([]byte(u))
+
+ var b strings.Builder
+ b.Grow(len(u)*2 + 512)
+ b.WriteString(`
`)
+ b.WriteString(`

+ b.WriteString(esc)
+ b.WriteString(`)
`)
+ b.WriteString(`
`)
+ if !disableReverse {
+ b.WriteString(`
🔍`)
+ }
+ if bookmarkingEnabled {
+ b.WriteString(`
`)
+ }
+ b.WriteString(`
`)
+ return b.String()
+}
+
+func writeChunkedCards(w http.ResponseWriter, q, next string, urls []string) {
+ if len(urls) == 0 {
+ return
+ }
+ if !chunkedMode || len(urls) == 1 {
+ for _, u := range urls {
+ _, _ = io.WriteString(w, renderCardHTML(q, next, u))
+ }
+ if f, ok := w.(http.Flusher); ok {
+ f.Flush()
+ }
+ return
+ }
+
+ type job struct {
+ idx int
+ u string
+ }
+ type result struct {
+ idx int
+ html string
+ }
+
+ jobs := make(chan job, len(urls))
+ results := make(chan result, len(urls))
+
+ workers := chunkWorkers
+ if workers > len(urls) {
+ workers = len(urls)
+ }
+ var wg sync.WaitGroup
+ wg.Add(workers)
+ for i := 0; i < workers; i++ {
+ go func() {
+ defer wg.Done()
+ for j := range jobs {
+ results <- result{idx: j.idx, html: renderCardHTML(q, next, j.u)}
+ }
+ }()
+ }
+
+ for i, u := range urls {
+ jobs <- job{idx: i, u: u}
+ }
+ close(jobs)
+
+ go func() {
+ wg.Wait()
+ close(results)
+ }()
+
+ out := make([]string, len(urls))
+ for r := range results {
+ out[r.idx] = r.html
+ }
+ for _, s := range out {
+ _, _ = io.WriteString(w, s)
+ }
+ if f, ok := w.(http.Flusher); ok {
+ f.Flush()
+ }
+}
+
// Index (front) - server-rendered bookmarks and settings form (no JS)
func indexHandler(w http.ResponseWriter, r *http.Request) {
accent, imgScale := getThemeVars(r)
@@ -521,6 +650,8 @@ func searchHandler(w http.ResponseWriter, r *http.Request) {
dec := json.NewDecoder(resp.Body)
var nextBookmark string
+ nextSearch := "/search?q=" + url.QueryEscape(q)
+ chunk := make([]string, 0, chunkSize)
for {
tk, err := dec.Token()
@@ -561,25 +692,17 @@ func searchHandler(w http.ResponseWriter, r *http.Request) {
if u == "" {
continue
}
- esc := url.QueryEscape(u)
- b64 := base64.StdEncoding.EncodeToString([]byte(u))
-
- // card
- _, _ = io.WriteString(w, ``)
- _, _ = io.WriteString(w, `

`)
- _, _ = io.WriteString(w, `
`)
- if !disableReverse {
- _, _ = io.WriteString(w, `
🔍`)
- }
- if bookmarkingEnabled {
- next := "/search?q=" + url.QueryEscape(q)
- _, _ = io.WriteString(w, `
`)
- }
- _, _ = io.WriteString(w, `
`) // card-controls
- _, _ = io.WriteString(w, `
`) // card
-
- if f, ok := w.(http.Flusher); ok {
- f.Flush()
+ if chunkedMode {
+ chunk = append(chunk, u)
+ if len(chunk) >= chunkSize {
+ writeChunkedCards(w, q, nextSearch, chunk)
+ chunk = chunk[:0]
+ }
+ } else {
+ _, _ = io.WriteString(w, renderCardHTML(q, nextSearch, u))
+ if f, ok := w.(http.Flusher); ok {
+ f.Flush()
+ }
}
}
_, _ = dec.Token()
@@ -595,6 +718,10 @@ func searchHandler(w http.ResponseWriter, r *http.Request) {
}
}
+ if chunkedMode && len(chunk) > 0 {
+ writeChunkedCards(w, q, nextSearch, chunk)
+ }
+
_, _ = io.WriteString(w, ``)
if nextBookmark != "" {
qenc := url.QueryEscape(q)
@@ -688,7 +815,7 @@ func revsearchHandler(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, tineye, http.StatusSeeOther)
}
-// ---------- bookmark handlers (unchanged from previous) ----------
+// ---------- bookmark handlers ----------
func bookmarkPostHandler(w http.ResponseWriter, r *http.Request) {
if !bookmarkingEnabled {
@@ -909,4 +1036,4 @@ func main() {
log.Println("Pinata listening on :8080 (no-JS mode). Bookmarking enabled:", bookmarkingEnabled, " Reverse disabled:", disableReverse)
log.Fatal(server.ListenAndServe())
-}
+}
\ No newline at end of file