git.sophuwu.com > mailboxxer   
              232
            
             package main

import (
	"bytes"
	"errors"
	"fmt"
	"golang.org/x/crypto/ssh/terminal"
	"golang.org/x/net/html"
	"io"
	"net/mail"
	"os"
	"os/exec"
	"path/filepath"
	"slices"
	"git.sophuwu.com/mailboxxer/db"
	"strings"
)

func GetTSize() (int, int) {
	w, h, err := terminal.GetSize(int(os.Stdin.Fd()))
	if err != nil {
		return 80, 24
	}
	return w, h
}

func displayAddress(a string) string {
	addr, err := mail.ParseAddress(a)
	if err != nil {
		return a
	}
	if addr.Name != "" {
		return addr.Name
	}
	return addr.Address
}

func node(w io.Writer, n *html.Node) {
	switch n.Type {
	case html.ElementNode:
		switch n.Data {
		case "script", "style", "head", "img":
			return
		case "b", "strong":
			fmt.Fprint(w, "\033[1m") // Bold
		case "i":
			fmt.Fprint(w, "\033[2m") // Italic
		}
		for c := n.FirstChild; c != nil; c = c.NextSibling {
			node(w, c)
		}

		// Reset styles after the element
		switch n.Data {
		case "b", "strong", "i", "em":
			fmt.Fprint(w, "\033[0m")
		}

	case html.TextNode:
		fmt.Fprint(w, n.Data, " ")

	default:
		for c := n.FirstChild; c != nil; c = c.NextSibling {
			node(w, c)
		}
	}

}

func RenderHTML(htmlContent string) string {
	doc, err := html.Parse(bytes.NewBufferString(htmlContent))
	if err != nil {
		panic(err)
	}

	var buff bytes.Buffer
	node(&buff, doc)
	return buff.String()
}

func DisplayRows(q *db.Query) {
	fmt.Printf("Page: %d/%d\n", q.Page(), q.TotalPages())
	fmt.Printf(" id | %13.13s | %40.40s | %s\n", "Date", "From", "Subject")
	for i, em := range q.Rows() {
		fmt.Printf("%3d | %13.13s | %40.40s | %s\n", i, db.TimeStr(em.Date), displayAddress(em.From), em.Subject)
	}
}

func wrap(w int, s string) string {
	var line = ""
	var lines, b []string
	for _, n := range strings.Split(s, "\n") {
		b = strings.Split(n, " ")
		for i, v := range b {
			if len(v) > w {
				lines = append(lines, v[:w-1], v[w-1:])
				continue
			}
			if len(line)+len(v) < w {
				line += v + " "
			} else {
				lines = append(lines, strings.TrimSuffix(line, " "))
				line = ""
				continue
			}
			if i == len(b)-1 {
				lines = append(lines, strings.TrimSuffix(line, " "))
				line = ""
			}
		}
	}
	lines = slices.Clip(lines)
	return "  " + strings.Join(lines, "  \n  ")
}

var ErrInvalidNumber = errors.New("invalid number")

func OpenMail(r *db.Query, s string) error {
	n, err := parseInt(s)
	if err != nil {
		return ErrInvalidNumber
	}
	var meta db.EmailMeta
	meta, err = r.Row(n)
	if err != nil {
		return ErrInvalidNumber
	}
	id := meta.Id
	var b []byte
	isHtml := false
	b, err = os.ReadFile(filepath.Join(db.SAVEPATH, id, "body.txt"))
	if errors.Is(err, os.ErrNotExist) {
		b, err = os.ReadFile(filepath.Join(db.SAVEPATH, id, "body.html"))
		isHtml = true
	}
	if err != nil {
		return err
	}
	s = string(b)
	if isHtml {
		s = RenderHTML(s)
	}
	cmd := exec.Command("less", "-sR")
	cmd.Stdin = strings.NewReader(s)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	fmt.Print("\033[?1049l")
	err = cmd.Run()
	fmt.Print("\033[?1049h")
	return err
}

func getSize(W, H *int) bool {
	w, h := GetTSize()
	if h > 28 {
		h = 28
	} else if h < 5 {
		h = 5
	}
	h -= 4
	b := false
	if H != nil {
		if *H != h {
			b = true
		}
		*H = h
	}
	if W != nil {
		*W = w
	}
	return b
}

func parseInt(s string) (int, error) {
	n := 0
	for _, r := range s {
		if r < '0' || r > '9' {
			return 0, fmt.Errorf("invalid number: %s", s)
		}
		n = n*10 + int(r-'0')
	}
	return n, nil
}

func CLI() {
	var s string
	var w, h int
	getSize(&w, &h)
	r, err := db.NewQuery(h)
	if err != nil {
		fmt.Fprintln(os.Stderr, "Error creating query:", err)
		return
	}

	// alternative screen
	fmt.Print("\033[?1049h")
	// on exit restore terminal
	defer fmt.Print("\033[?1049l")

	for {
		fmt.Print("\033[2J\033[H\r")
		if getSize(&w, &h) {
			if err = r.SetPageSize(h); err != nil {
				break
			}
		}
		DisplayRows(r)
		fmt.Println("n: next page, p: previous  |  q: quit  |  <id>: open")
		fmt.Print("input (n/p/q/id): ")
		s = ""
		fmt.Scanln(&s)
		switch s {
		case "":
			continue
		case "q":
			return
		case "n":
			err = r.Next()
		case "p":
			err = r.Prev()
		default:
			if err = OpenMail(r, s); errors.Is(err, ErrInvalidNumber) {
				continue
			}
		}
		if err != nil {
			fmt.Fprintln(os.Stderr, "Error:", err)
			return
		}

	}
}