package main import ( "bytes" _ "embed" "fmt" "github.com/mholt/archiver/v4" "log" "net/http" "os" "os/exec" "regexp" "strings" ) //go:embed index.html var index []byte //go:embed font.css var font []byte //go:embed dark_theme.css var css []byte //go:embed favicon.ico var favicon []byte var CFG struct { Hostname string ListenAddr string ListenPort string MANPATH string Pandoc string } func cmdout(s string) string { ss := strings.Split(s, " ") b, e := exec.Command(ss[0], ss[1:]...).Output() if e != nil { log.Fatal("Fatal: unable to get " + ss[0]) } return strings.TrimSpace(string(b)) } func init() { CFG.MANPATH = cmdout("manpath") CFG.Hostname = cmdout("hostname") CFG.Pandoc = cmdout("which pandoc") CFG.ListenAddr = os.Getenv("ListenAddr") CFG.ListenPort = os.Getenv("ListenPort") if CFG.ListenPort == "" { CFG.ListenPort = "8082" } css = append(css, font...) } func main() { http.HandleFunc("/style.css", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/css; charset=utf-8") w.Header().Set("Content-Length", fmt.Sprint(len(css))) w.WriteHeader(http.StatusOK) w.Write(css) }) http.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "image/x-icon") w.Header().Set("Content-Length", fmt.Sprint(len(favicon))) w.WriteHeader(http.StatusOK) w.Write(favicon) }) http.HandleFunc("/", indexHandler) http.ListenAndServe(CFG.ListenAddr+":"+CFG.ListenPort, nil) } const htmlHeader = ` {{ title }} ` func HtmlHeader(body *string, title string) { *body = strings.ReplaceAll(htmlHeader, "{{ title }}", strings.ToUpper(title)) + *body + "" *body = strings.Replace(*body, "

NAME

", "

"+strings.ToUpper(title)+"

", 1) } type ManPage struct { Section int Name string Path string } func readCompressed(fh *os.File, buff *bytes.Buffer) error { decompressor, err := archiver.Gz{}.OpenReader(fh) if err != nil { return err } defer decompressor.Close() _, err = buff.ReadFrom(decompressor) return err } func ReadFh(path string) (string, error) { var buff bytes.Buffer fh, err := os.OpenFile(path, os.O_RDONLY, 0) if err != nil { return "", err } defer fh.Close() if strings.HasSuffix(path, ".gz") { err = readCompressed(fh, &buff) return buff.String(), err } _, err = buff.ReadFrom(fh) return buff.String(), err } func pandocConvert(input string) (string, error) { cmd := exec.Command(CFG.Pandoc, "--section-divs", "-t", "html4", "-f", "man") cmd.Env = append(cmd.Env, os.Environ()...) cmd.Stdin = strings.NewReader(input) b, err := cmd.Output() return string(b), err } func (m *ManPage) html(w http.ResponseWriter, r *http.Request) error { if m.Path == "" { return fmt.Errorf("no path") } var b, fh string var err error fh, err = ReadFh(m.Path) if err != nil { return err } b, err = pandocConvert(fh) if err != nil { return err } HtmlHeader(&b, m.Name) w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(http.StatusOK) fmt.Fprint(w, b) return nil } func (m *ManPage) FindPath() error { s := m.Name if m.Section > 0 { s += "." + fmt.Sprint(m.Section) } cmd := exec.Command("man", "-w", s) b, e := cmd.Output() if e != nil { return fmt.Errorf("page not found") } m.Path = strings.TrimSpace(string(b)) return nil } var manRegexp = []*regexp.Regexp{regexp.MustCompile(`\.[1-9]$`), regexp.MustCompile(`( )?[(][1-9][)]$`)} func (m *ManPage) ParseName(s string) (err error) { s = strings.TrimSpace(s) for i, rx := range manRegexp { if rx.MatchString(s) { m.Section = int((s[len(s)-i-1]) - '0') m.Name = strings.TrimSpace(s[:len(s)-i-2]) return m.FindPath() } } m.Section = 0 m.Name = s return m.FindPath() } func searchHandler(w http.ResponseWriter, r *http.Request) { r.ParseForm() args := "-l\n-" + strings.Join(r.Form["arg"], "\n-") search := strings.ReplaceAll(r.Form["search"][0], "\r", "") args += "\n" + search cmd := exec.Command("apropos", strings.Split(args, "\n")...) cmd.Env = append(cmd.Env, os.Environ()...) // cmd.Env = append(cmd.Env, "MANPATH="+CFG.MANPATH) b, e := cmd.Output() if e != nil { http.Error(w, "no results", http.StatusNotFound) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(http.StatusOK) fmt.Fprint(w, strings.ReplaceAll(string(b), "\n", "
")) } func indexHandler(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" { http.Redirect(w, r, "/", http.StatusTemporaryRedirect) return } if r.Method == "POST" { searchHandler(w, r) return } if !strings.HasPrefix(r.URL.RawQuery, "man=") && r.URL.RawQuery != "" { r.URL.RawQuery = "man=" + r.URL.RawQuery } _ = r.ParseForm() q := r.Form.Get("man") var man ManPage if err := man.ParseName(q); err != nil { w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(http.StatusOK) w.Write(bytes.ReplaceAll(index, []byte("{{ host }}"), []byte(r.Host))) return } fmt.Fprintf(w, "%v", man.html(w, r)) }