git.sophuwu.com > sketch   
              305
            
             package main

import (
	"fmt"
	"image"
	_ "image/jpeg"
	_ "image/png"
	"math"
	"os"
	"os/signal"
	"regexp"

	"github.com/nfnt/resize"
	"golang.org/x/sys/unix"
	"golang.org/x/term"
)

func getTermSize() (float64, float64) {
	W, H, err := term.GetSize(int(os.Stdout.Fd()))
	if err != nil || !(W > 0 && H > 0) {
		fmt.Fprintln(os.Stderr, "fatal: could not get terminal size")
		os.Exit(1)
	}
	return float64(W), float64(H)
}

func (img *Immg) OpenImg(path string) error {
	file, err := os.Open(path)
	if err != nil {
		return fmt.Errorf("error opening file: %s", path)
	}
	defer file.Close()
	img.Img, _, err = image.Decode(file)
	if err != nil {
		return fmt.Errorf("error decoding file: %s", path)
	}
	return nil
}

type Immg struct {
	Img   image.Image
	X     int
	Y     int
	Scale float64
}

func (img *Immg) SetScale(s string) {
	for i := len(s) - 1; 0 <= i; i-- {
		if '9' >= s[i] && '0' <= s[i] {
			img.Scale = (img.Scale + float64(s[i]-'0')) / 10
		}
	}
}

func (img *Immg) ForX() {
	img.X = 0
	for img.X < 2*(img.Img.Bounds().Dx()) {
		img.Doalp()
		img.X++
		img.Doalp()
		img.X++
		fmt.Print("▀\033[0m")
	}
}

func F(n int) float64 {
	return float64(n)
}

func fxf(a float64) func(b float64) uint {
	return func(b float64) uint {
		return uint(math.Round(a * b))
	}
}

func (img *Immg) Print() {
	w, h := getTermSize()
	b := img.Img.Bounds()
	x, y := F(b.Dx()), F(b.Dy())
	h = h*2 - 1
	if x > w {
		y, x = y*w/x, w
	}
	if y > h {
		y, x = h, x*h/y
	}
	u := fxf(img.Scale)
	img.Img = resize.Resize(u(x), u(y), img.Img, resize.MitchellNetravali)
	img.Y = img.Img.Bounds().Min.Y
	for img.Y < img.Img.Bounds().Max.Y {
		fmt.Print("\r")
		img.ForX()
		fmt.Println()
		img.Y += 2
	}
}

func (img *Immg) Doalp() {
	r, g, b, _ := img.Img.At(img.Img.Bounds().Min.X+img.X/2, img.Y+img.X%2).RGBA()
	fmt.Printf("\033[%d8;2;%d;%d;%dm", 3+(img.X%2), r>>8, g>>8, b>>8)
}

var scaleFlag = regexp.MustCompile(`-[0-9]+`)

func main() {
	if len(os.Args) == 1 {
		gallery()
		return
	}
	var img Immg
	img.Scale = 1
	var err error
	for _, path := range os.Args[1:] {
		if scaleFlag.MatchString(path) {
			img.SetScale(path)
			continue
		}
		err = img.OpenImg(path)
		if err != nil {
			fmt.Fprintf(os.Stderr, "error opening image: %s\n", path)
			continue
		}
		img.Print()
		if len(os.Args) > 2 {
			fmt.Println()
		}
	}
}

func gallery() {
	g := Gallary{}
	err := g.Load()
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
	defer g.deferred()
	g.Print()
	ch := make(chan os.Signal, 1)
	signal.Notify(ch, unix.SIGWINCH)
	go func() {
		for range ch {
			g.Print()
		}
	}()

	buf := make([]byte, 4)
	var n int
	for {
		n, err = os.Stdin.Read(buf)
		if err != nil {
			break
		}
		g.HandleInput(string(buf[:n]))

	}
}

func (g *Gallary) HandleInput(s string) {
	switch s {
	case "q", "Q", "\x03", "\x04":
		g.Quit()
	case "\x1b[C":
		g.Next()
	case "\x1b[D":
		g.Prev()
	case "\x1b[A", "+":
		g.ChangeScale(0.05)
	case "\x1b[B", "-":
		g.ChangeScale(-0.05)
	case "0":
		g.Img.Scale = 1
		g.Print()
	case "\r", " ":
		g.ShowMeta = !g.ShowMeta
		g.Print()
	}
}

func (g *Gallary) ChangeScale(n float64) {
	g.Img.Scale += n
	if g.Img.Scale < 0.05 {
		g.Img.Scale = 0.05
	}
	if g.Img.Scale > 0.95 {
		g.Img.Scale = 0.95
	}
	g.Print()
}

func (g *Gallary) Next() {
	g.Index++
	if g.Index >= len(g.Paths) {
		g.Index = 0
	}
	g.Print()
}
func (g *Gallary) Prev() {
	g.Index--
	if g.Index < 0 {
		g.Index = len(g.Paths) - 1
	}
	g.Print()
}

func (g *Gallary) Quit() {
	g.deferred()
	os.Exit(0)
}

type Gallary struct {
	Paths    []string
	Index    int
	Img      Immg
	ShowMeta bool
}

var oldState *term.State

func (g *Gallary) Load() error {
	de, err := os.ReadDir(".")
	if err != nil {
		return fmt.Errorf("fatal: could not read current directory")
	}
	paths := []string{}
	rx := regexp.MustCompile(`(?i)\.(png|jpe?g)$`).MatchString
	for _, entry := range de {
		if entry.IsDir() {
			continue
		}
		if entry.Type().IsRegular() && rx(entry.Name()) {
			paths = append(paths, entry.Name())
		}
	}
	if len(paths) == 0 {
		return fmt.Errorf("fatal: no images found in current directory")
	}
	g.Paths = paths
	g.Index = 0
	g.Img.Scale = 0.8
	err = g.Img.OpenImg(g.Paths[g.Index])
	if err != nil {
		return fmt.Errorf("error opening image: %s", g.Paths[g.Index])
	}

	oldState, err = term.MakeRaw(int(os.Stdin.Fd()))
	if err != nil {
		return err
	}
	fmt.Print("\033[?1049h")
	return nil
}
func (g *Gallary) deferred() {
	if r := recover(); r != nil {
		term.Restore(int(os.Stdin.Fd()), oldState) // restore terminal when program exits
		fmt.Print("\033[?1049l")
		panic(r)
	} else {
		term.Restore(int(os.Stdin.Fd()), oldState) // restore terminal when program exits
		fmt.Print("\033[?1049l")
	}
}

func humanBytes(n int64) string {
	f := float64(n)
	prefix := "-KMGTPE"
	i := 0
	for f >= 1024 && i < len(prefix)-1 {
		i++
		f /= 1024
	}
	if i == 0 {
		return fmt.Sprintf("%d B", n)
	}
	return fmt.Sprintf("%.1f %ciB", f, prefix[i])
}

func (g *Gallary) Print() {
	err := g.Img.OpenImg(g.Paths[g.Index])
	if err != nil {
		g.Paths = append(g.Paths[:g.Index], g.Paths[g.Index+1:]...)
		g.Next()
		return
	}
	s := ""
	if g.ShowMeta {
		resx := g.Img.Img.Bounds().Dx()
		resy := g.Img.Img.Bounds().Dy()
		name := g.Paths[g.Index]
		st, _ := os.Stat(name)
		size := st.Size()

		s = fmt.Sprintf("%s (%dx%d, %s)", name, resx, resy, humanBytes(size))
	}
	fmt.Println("\033[2J\033[1;1H")
	g.Img.Print()
	y := g.Img.Y/2 + g.Img.Y%2 + 2
	if g.ShowMeta {
		fmt.Printf("\033[%d;1H%s", y, s)
	} else {
		fmt.Printf("\033[%d;1H%s\t%s\t%s\t%s", y, "[Q] quit", "[←] [→] navigation", "[↑] [↓] scale", "[Space] toggle info")
	}
}

var uwu map[string]func(s []string)