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)