// main.go package main import ( "bufio" "embed" "encoding/json" "fmt" "io" "io/fs" "log" "net" "net/http" "os" "path/filepath" "regexp" "runtime" "strings" "time" webview "github.com/webview/webview_go" ) //go:embed index.html ui/* images/* var content embed.FS type saveReq struct { Filename string `json:"filename"` Content string `json:"content"` } type saveResp struct { Path string `json:"path"` } func main() { // Static files subFS, err := fs.Sub(content, ".") if err != nil { log.Fatalf("fs.Sub error: %v", err) } mux := http.NewServeMux() mux.Handle("/", http.FileServer(http.FS(subFS))) // /save endpoint writes to Downloads mux.HandleFunc("/save", func(w http.ResponseWriter, r *http.Request) { start := time.Now() defer func() { log.Printf("%s %s %s", r.Method, r.URL.Path, time.Since(start)) }() if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } var req saveReq if err := json.NewDecoder(io.LimitReader(r.Body, 10<<20)).Decode(&req); err != nil { http.Error(w, "bad json", http.StatusBadRequest) return } if req.Filename == "" { req.Filename = fmt.Sprintf("relibre-%d.html", time.Now().Unix()) } req.Filename = sanitizeFilename(req.Filename) dir := downloadsDir() if err := os.MkdirAll(dir, 0o755); err != nil { http.Error(w, "mkdir failed", http.StatusInternalServerError) return } path := filepath.Join(dir, req.Filename) if err := os.WriteFile(path, []byte(req.Content), 0o644); err != nil { http.Error(w, "write failed", http.StatusInternalServerError) return } log.Printf("Saved HTML -> %s", path) w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(saveResp{Path: path}) }) // Simple request logger handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() mux.ServeHTTP(w, r) log.Printf("%s %s %s", r.Method, r.URL.Path, time.Since(start)) }) // Start server on ephemeral port ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { log.Fatalf("listen: %v", err) } addr := ln.Addr().String() srv := &http.Server{Handler: handler, ReadHeaderTimeout: 5 * time.Second} go func() { log.Printf("HTTP listening on http://%s", addr) if err := srv.Serve(ln); err != nil && err != http.ErrServerClosed { log.Fatalf("http serve: %v", err) } }() // Native webview window (debug true for dev tools/log signals) debug := true w := webview.New(debug) defer w.Destroy() w.SetTitle("Relibre") w.SetSize(1100, 750, webview.HintNone) w.Navigate(fmt.Sprintf("http://%s", addr)) w.Run() _ = srv.Close() if runtime.GOOS == "linux" { time.Sleep(100 * time.Millisecond) } } func sanitizeFilename(name string) string { name = strings.TrimSpace(name) if name == "" { return "relibre.html" } if !strings.HasSuffix(strings.ToLower(name), ".html") { name += ".html" } re := regexp.MustCompile(`[<>:"/\\|?*\x00-\x1F]`) return re.ReplaceAllString(name, "_") } func downloadsDir() string { if p := readXDGDownloads(); p != "" { return p } home, _ := os.UserHomeDir() if home == "" { return "./" } return filepath.Join(home, "Downloads") } func readXDGDownloads() string { home, _ := os.UserHomeDir() if home == "" { return "" } path := filepath.Join(home, ".config", "user-dirs.dirs") f, err := os.Open(path) if err != nil { return "" } defer f.Close() sc := bufio.NewScanner(f) for sc.Scan() { line := sc.Text() if strings.HasPrefix(line, "XDG_DOWNLOAD_DIR=") { raw := strings.TrimPrefix(line, "XDG_DOWNLOAD_DIR=") raw = strings.Trim(raw, `"`) raw = strings.ReplaceAll(raw, "$HOME", home) return raw } } return "" }