From e001cf7f742b94d5025708e03f09b29d0c2713b7 Mon Sep 17 00:00:00 2001 From: Rose Hogenson Date: Fri, 12 Jun 2026 21:03:07 -0700 Subject: Add ils to imgutil --- README.md | 11 +- go.mod | 2 +- ils.go | 459 ------------------------------------------------------------- ils/ils.go | 459 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 468 insertions(+), 463 deletions(-) delete mode 100644 ils.go create mode 100644 ils/ils.go diff --git a/README.md b/README.md index 91feeb3..df587a8 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,12 @@ -# ils +# Image utilities for the command line -Like ls but for images +Included utilities: + + - ils: like ls but for images + - icat: display images in the terminal + - convert: convert between formats or resize images + - itest: check image properties ``` -go install roseh.moe/cmd/ils@latest +go install roseh.moe/cmd/imgutil/...@latest ``` diff --git a/go.mod b/go.mod index 8cdc31c..3693705 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module roseh.moe/cmd/ils +module roseh.moe/cmd/imgutil go 1.26.3 diff --git a/ils.go b/ils.go deleted file mode 100644 index f2301dd..0000000 --- a/ils.go +++ /dev/null @@ -1,459 +0,0 @@ -package main - -import ( - "bufio" - "flag" - "fmt" - "image" - "image/color" - _ "image/gif" - _ "image/jpeg" - _ "image/png" - "io" - "os" - "path/filepath" - "slices" - "strconv" - - runewidth "github.com/mattn/go-runewidth" - _ "golang.org/x/image/bmp" - "golang.org/x/image/draw" - _ "golang.org/x/image/tiff" - _ "golang.org/x/image/webp" - "golang.org/x/sys/unix" - "roseh.moe/pkg/sixel" -) - -var ( - x = flag.Int("x", 15, "set image width in columns") - l = flag.Bool("l", false, "print image attributes") - m = flagPrintMode(flag.CommandLine, "m", modeSixel, "one of 'block', 'block24', or 'sixel'") -) - -type printMode int - -const ( - modeInvalid printMode = iota - modeBlock - modeBlock24 - modeSixel -) - -type printModeValue printMode - -func (m *printModeValue) String() string { - switch printMode(*m) { - case modeBlock: - return "block" - case modeBlock24: - return "block24" - case modeSixel: - return "sixel" - default: - return "invalid" - } -} - -func (m *printModeValue) Set(s string) error { - var mode printMode - switch s { - case "block": - mode = modeBlock - case "block24": - mode = modeBlock24 - case "sixel": - mode = modeSixel - default: - return fmt.Errorf("bad mode type %q, should be one of 'block', 'block24', or 'sixel'", s) - } - *m = printModeValue(mode) - return nil -} - -func flagPrintMode(fs *flag.FlagSet, name string, value printMode, usage string) *printMode { - fs.Var((*printModeValue)(&value), name, usage) - return &value -} - -func isDir(filename string) bool { - stat, err := os.Stat(filename) - return err == nil && stat.IsDir() -} - -func isImage(filename string) bool { - f, err := os.Open(filename) - if err != nil { - return false - } - defer f.Close() - _, _, err = image.DecodeConfig(f) - return err == nil -} - -type imageMetadata struct { - format, colorModel string - width, height int -} - -func (m *imageMetadata) String() string { - return fmt.Sprintf("%dx%d %s %s", m.width, m.height, m.format, m.colorModel) -} - -func colorModelName(m color.Model) string { - switch m { - case color.RGBAModel: - return "rgba" - case color.RGBA64Model: - return "rgba 64 bit" - case color.NRGBAModel: - return "rgba not alpha premultiplied" - case color.NRGBA64Model: - return "rgba 64 bit not alpha premultiplied" - case color.AlphaModel: - return "alpha" - case color.Alpha16Model: - return "alpha 16 bit" - case color.GrayModel: - return "grayscale" - case color.Gray16Model: - return "grayscale 16 bit" - case color.CMYKModel: - return "cmyk" - case color.NYCbCrAModel: - return "Y'CbCr with alpha" - case color.YCbCrModel: - return "Y'CbCr" - } - if palette, ok := m.(color.Palette); ok { - return fmt.Sprintf("palettized %d colors", len(palette)) - } - return fmt.Sprintf("unknown %v", m) -} - -func readMetadata(filename string) (imageMetadata, error) { - f, err := os.Open(filename) - if err != nil { - return imageMetadata{}, err - } - defer f.Close() - config, format, err := image.DecodeConfig(f) - if err != nil { - return imageMetadata{}, err - } - return imageMetadata{format: format, colorModel: colorModelName(config.ColorModel), width: config.Width, height: config.Height}, nil -} - -func load(filename string) (image.Image, error) { - f, err := os.Open(filename) - if err != nil { - return nil, err - } - defer f.Close() - - img, _, err := image.Decode(f) - if err != nil { - return nil, fmt.Errorf("decode %q: %s", filename, err) - } - - return img, nil -} - -func printImg(w io.Writer, img image.Image) { - switch *m { - case modeBlock: - sixel.PrintXTerm16(w, img) - case modeBlock24: - sixel.PrintBlock(w, img) - case modeSixel: - sixel.Print(w, img, nil) - default: - panic("invalid mode") - } -} - -func escapeString(s string) string { - for _, r := range s { - if !strconv.IsGraphic(r) { - return strconv.QuoteToGraphic(s) - } - } - return s -} - -func divRound(n, d int) int { - return (n + d/2) / d -} - -type pixelAspectRatio struct { - x, y int -} - -func layoutRow(imagePaths []string, metadata []imageMetadata, width, rowWidth, cellX int) []int { - filenameWidths := make([]int, len(imagePaths)) - for i, path := range imagePaths { - filenameWidths[i] = cellX * runewidth.StringWidth(escapeString(filepath.Base(path))) - if *l { - filenameWidths[i] = max(filenameWidths[i], cellX*runewidth.StringWidth(metadata[i].String())) - } - } - var logicalWidth int - for i, filenameWidth := range filenameWidths { - if i > 0 { - logicalWidth += 2 * cellX - } - logicalWidth += max(width, filenameWidth) - } - layout := make([]int, len(imagePaths)) - var offset int - for i, filenameWidth := range filenameWidths { - imageWidth := max(width, filenameWidth) - layout[i] = divRound(offset*rowWidth, logicalWidth) - offset += imageWidth + 2*cellX - } - return layout -} - -func compositeImageRow(imagePaths []string, metadata []imageMetadata, width, rowWidth, cellX int, aspectRatio pixelAspectRatio) (image.Image, error) { - if len(metadata) == 0 { - metadata = make([]imageMetadata, len(imagePaths)) - for i, filename := range imagePaths { - var err error - if metadata[i], err = readMetadata(filename); err != nil { - return nil, err - } - } - } - var maxHeight int - for _, fileMetadata := range metadata { - maxHeight = max(maxHeight, divRound(fileMetadata.height*width*aspectRatio.x, fileMetadata.width*aspectRatio.y)) - } - layout := layoutRow(imagePaths, metadata, width, rowWidth, cellX) - bounds := image.Rect(0, 0, rowWidth, maxHeight) - result := image.NewRGBA(bounds) - for i, filename := range imagePaths { - img, err := load(filename) - if err != nil { - return nil, err - } - start := layout[i] - end := rowWidth - if i+1 < len(imagePaths) { - end = layout[i+1] - } - height := divRound(img.Bounds().Dy()*width*aspectRatio.x, img.Bounds().Dx()*aspectRatio.y) - offsetStart := start + (end-start-width)/2 - dstBounds := image.Rect(offsetStart, (maxHeight-height)/2, offsetStart+width, (maxHeight-height)/2+height) - draw.BiLinear.Scale(result, dstBounds, img, img.Bounds(), draw.Src, nil) - } - return result, nil -} - -func splitRows(imagePaths []string, metadata []imageMetadata, width, rowWidth int) [][]string { - var rows [][]string - for len(imagePaths) > 0 { - row := imagePaths[:0] - thisRowWidth := 0 - for ; len(row) < len(imagePaths); row = row[:len(row)+1] { - nextW := max(width, runewidth.StringWidth(escapeString(filepath.Base(imagePaths[len(row)])))) - if *l { - nextW = max(nextW, len(metadata[len(row)].String())) - } - if len(row) > 0 { - nextW += 2 - } - if thisRowWidth > 0 && thisRowWidth+nextW > rowWidth { - break - } - thisRowWidth += nextW - } - imagePaths = imagePaths[len(row):] - if *l { - metadata = metadata[len(row):] - } - rows = append(rows, row) - } - return rows -} - -func thumbnailFiles(w io.Writer, imagePaths []string, width, rowWidth, cellX, cellY int) error { - padding := 2 - aspectRatio := pixelAspectRatio{1, 1} - switch *m { - case modeBlock: - aspectRatio = pixelAspectRatio{cellX, cellY} - cellX = 1 - case modeBlock24: - aspectRatio = pixelAspectRatio{2 * cellX, cellY} - cellX = 1 - case modeSixel: - padding *= cellX - } - var metadata []imageMetadata - if *l { - metadata = make([]imageMetadata, len(imagePaths)) - for i, filename := range imagePaths { - var err error - if metadata[i], err = readMetadata(filename); err != nil { - return err - } - } - } - rows := splitRows(imagePaths, metadata, width, rowWidth) - rowMetadata := make([][]imageMetadata, len(rows)) - if *l { - for i, row := range rows { - rowMetadata[i] = metadata[:len(row)] - metadata = metadata[len(row):] - } - } - rowImages := make([]chan image.Image, len(rows)) - for i := range rowImages { - rowImages[i] = make(chan image.Image, 1) - } - errCh := make(chan error, 1) - go func() { - sem := make(chan struct{}, 20) - for i, row := range rows { - sem <- struct{}{} - go func() { - defer func() { <-sem }() - img, err := compositeImageRow(row, rowMetadata[i], width*cellX, rowWidth*cellX, cellX, aspectRatio) - if err != nil { - select { - case errCh <- err: - default: - } - return - } - rowImages[i] <- img - }() - } - }() - for i, row := range rows { - var rowImage image.Image - select { - case err := <-errCh: - return err - case rowImage = <-rowImages[i]: - } - printImg(w, rowImage) - fmt.Fprintln(w) - layouts := layoutRow(row, rowMetadata[i], width*cellX, rowWidth*cellX, cellX) - realOffset := 0 - for j, filename := range row { - start := divRound(layouts[j], cellX) - end := rowWidth - if j+1 < len(row) { - end = divRound(layouts[j+1], cellX) - } - fmt.Fprintf(w, "%*s", start-realOffset, "") - realOffset = start - fileNameWidth := runewidth.StringWidth(escapeString(filepath.Base(filename))) - padding := max((end-start-fileNameWidth)/2, 0) - fmt.Fprintf(w, "%*s%s", padding, "", escapeString(filepath.Base(filename))) - realOffset += padding + fileNameWidth - } - fmt.Fprintln(w) - if *l { - realOffset = 0 - for j, metadata := range rowMetadata[i] { - start := divRound(layouts[j], cellX) - end := rowWidth - if j+1 < len(row) { - end = divRound(layouts[j+1], cellX) - } - fmt.Fprintf(w, "%*s", start-realOffset, "") - realOffset = start - details := metadata.String() - padding := max((end-start-len(details))/2, 0) - fmt.Fprintf(w, "%*s%s", padding, "", details) - realOffset += padding + len(details) - } - fmt.Fprintln(w) - } - } - return nil -} - -func readDir(d string) ([]string, error) { - files, err := os.ReadDir(d) - if err != nil { - return nil, err - } - filenames := make([]string, len(files)) - for i, f := range files { - filenames[i] = filepath.Join(d, f.Name()) - } - return filenames, nil -} - -func thumbnailDir(w io.Writer, dir string, width, rowWidth, cellX, cellY int) error { - files, err := readDir(dir) - if err != nil { - return err - } - files = slices.DeleteFunc(files, func(filename string) bool { return !isImage(filename) }) - return thumbnailFiles(w, files, width, rowWidth, cellX, cellY) -} - -func thumbnailer(args []string) error { - cols := *x - if *x <= 0 { - return fmt.Errorf("-x must be greater than 0") - } - ws, err := unix.IoctlGetWinsize(int(os.Stdout.Fd()), unix.TIOCGWINSZ) - if err != nil { - return fmt.Errorf("terminal size: %s", err) - } - termCols := int(ws.Col) - cellX, cellY := int(ws.Xpixel)/int(ws.Col), int(ws.Ypixel)/int(ws.Row) - - cols = min(cols, termCols) - - var files, dirs []string - if len(args) == 0 { - dirs = []string{"."} - } else { - for _, arg := range args { - if isDir(arg) { - dirs = append(dirs, arg) - } else { - files = append(files, arg) - } - } - } - w := bufio.NewWriter(os.Stdout) - defer w.Flush() - if err := thumbnailFiles(w, files, cols, termCols, cellX, cellY); err != nil { - return err - } - if len(files) > 0 && len(dirs) > 0 { - fmt.Fprintln(w) - } - for i, d := range dirs { - if i > 0 { - fmt.Fprintln(w) - } - if len(dirs) > 1 { - fmt.Fprintf(w, "%s:\n", d) - } - if err := thumbnailDir(w, d, cols, termCols, cellX, cellY); err != nil { - return err - } - } - - return nil -} - -func main() { - flag.Usage = func() { - fmt.Fprintf(os.Stderr, "Usage: thumbnailer [OPTION]... [FILE]...\n") - flag.PrintDefaults() - } - flag.Parse() - - if err := thumbnailer(flag.Args()); err != nil { - fmt.Fprintf(os.Stderr, "Failed: %s\n", err) - os.Exit(1) - } -} diff --git a/ils/ils.go b/ils/ils.go new file mode 100644 index 0000000..f2301dd --- /dev/null +++ b/ils/ils.go @@ -0,0 +1,459 @@ +package main + +import ( + "bufio" + "flag" + "fmt" + "image" + "image/color" + _ "image/gif" + _ "image/jpeg" + _ "image/png" + "io" + "os" + "path/filepath" + "slices" + "strconv" + + runewidth "github.com/mattn/go-runewidth" + _ "golang.org/x/image/bmp" + "golang.org/x/image/draw" + _ "golang.org/x/image/tiff" + _ "golang.org/x/image/webp" + "golang.org/x/sys/unix" + "roseh.moe/pkg/sixel" +) + +var ( + x = flag.Int("x", 15, "set image width in columns") + l = flag.Bool("l", false, "print image attributes") + m = flagPrintMode(flag.CommandLine, "m", modeSixel, "one of 'block', 'block24', or 'sixel'") +) + +type printMode int + +const ( + modeInvalid printMode = iota + modeBlock + modeBlock24 + modeSixel +) + +type printModeValue printMode + +func (m *printModeValue) String() string { + switch printMode(*m) { + case modeBlock: + return "block" + case modeBlock24: + return "block24" + case modeSixel: + return "sixel" + default: + return "invalid" + } +} + +func (m *printModeValue) Set(s string) error { + var mode printMode + switch s { + case "block": + mode = modeBlock + case "block24": + mode = modeBlock24 + case "sixel": + mode = modeSixel + default: + return fmt.Errorf("bad mode type %q, should be one of 'block', 'block24', or 'sixel'", s) + } + *m = printModeValue(mode) + return nil +} + +func flagPrintMode(fs *flag.FlagSet, name string, value printMode, usage string) *printMode { + fs.Var((*printModeValue)(&value), name, usage) + return &value +} + +func isDir(filename string) bool { + stat, err := os.Stat(filename) + return err == nil && stat.IsDir() +} + +func isImage(filename string) bool { + f, err := os.Open(filename) + if err != nil { + return false + } + defer f.Close() + _, _, err = image.DecodeConfig(f) + return err == nil +} + +type imageMetadata struct { + format, colorModel string + width, height int +} + +func (m *imageMetadata) String() string { + return fmt.Sprintf("%dx%d %s %s", m.width, m.height, m.format, m.colorModel) +} + +func colorModelName(m color.Model) string { + switch m { + case color.RGBAModel: + return "rgba" + case color.RGBA64Model: + return "rgba 64 bit" + case color.NRGBAModel: + return "rgba not alpha premultiplied" + case color.NRGBA64Model: + return "rgba 64 bit not alpha premultiplied" + case color.AlphaModel: + return "alpha" + case color.Alpha16Model: + return "alpha 16 bit" + case color.GrayModel: + return "grayscale" + case color.Gray16Model: + return "grayscale 16 bit" + case color.CMYKModel: + return "cmyk" + case color.NYCbCrAModel: + return "Y'CbCr with alpha" + case color.YCbCrModel: + return "Y'CbCr" + } + if palette, ok := m.(color.Palette); ok { + return fmt.Sprintf("palettized %d colors", len(palette)) + } + return fmt.Sprintf("unknown %v", m) +} + +func readMetadata(filename string) (imageMetadata, error) { + f, err := os.Open(filename) + if err != nil { + return imageMetadata{}, err + } + defer f.Close() + config, format, err := image.DecodeConfig(f) + if err != nil { + return imageMetadata{}, err + } + return imageMetadata{format: format, colorModel: colorModelName(config.ColorModel), width: config.Width, height: config.Height}, nil +} + +func load(filename string) (image.Image, error) { + f, err := os.Open(filename) + if err != nil { + return nil, err + } + defer f.Close() + + img, _, err := image.Decode(f) + if err != nil { + return nil, fmt.Errorf("decode %q: %s", filename, err) + } + + return img, nil +} + +func printImg(w io.Writer, img image.Image) { + switch *m { + case modeBlock: + sixel.PrintXTerm16(w, img) + case modeBlock24: + sixel.PrintBlock(w, img) + case modeSixel: + sixel.Print(w, img, nil) + default: + panic("invalid mode") + } +} + +func escapeString(s string) string { + for _, r := range s { + if !strconv.IsGraphic(r) { + return strconv.QuoteToGraphic(s) + } + } + return s +} + +func divRound(n, d int) int { + return (n + d/2) / d +} + +type pixelAspectRatio struct { + x, y int +} + +func layoutRow(imagePaths []string, metadata []imageMetadata, width, rowWidth, cellX int) []int { + filenameWidths := make([]int, len(imagePaths)) + for i, path := range imagePaths { + filenameWidths[i] = cellX * runewidth.StringWidth(escapeString(filepath.Base(path))) + if *l { + filenameWidths[i] = max(filenameWidths[i], cellX*runewidth.StringWidth(metadata[i].String())) + } + } + var logicalWidth int + for i, filenameWidth := range filenameWidths { + if i > 0 { + logicalWidth += 2 * cellX + } + logicalWidth += max(width, filenameWidth) + } + layout := make([]int, len(imagePaths)) + var offset int + for i, filenameWidth := range filenameWidths { + imageWidth := max(width, filenameWidth) + layout[i] = divRound(offset*rowWidth, logicalWidth) + offset += imageWidth + 2*cellX + } + return layout +} + +func compositeImageRow(imagePaths []string, metadata []imageMetadata, width, rowWidth, cellX int, aspectRatio pixelAspectRatio) (image.Image, error) { + if len(metadata) == 0 { + metadata = make([]imageMetadata, len(imagePaths)) + for i, filename := range imagePaths { + var err error + if metadata[i], err = readMetadata(filename); err != nil { + return nil, err + } + } + } + var maxHeight int + for _, fileMetadata := range metadata { + maxHeight = max(maxHeight, divRound(fileMetadata.height*width*aspectRatio.x, fileMetadata.width*aspectRatio.y)) + } + layout := layoutRow(imagePaths, metadata, width, rowWidth, cellX) + bounds := image.Rect(0, 0, rowWidth, maxHeight) + result := image.NewRGBA(bounds) + for i, filename := range imagePaths { + img, err := load(filename) + if err != nil { + return nil, err + } + start := layout[i] + end := rowWidth + if i+1 < len(imagePaths) { + end = layout[i+1] + } + height := divRound(img.Bounds().Dy()*width*aspectRatio.x, img.Bounds().Dx()*aspectRatio.y) + offsetStart := start + (end-start-width)/2 + dstBounds := image.Rect(offsetStart, (maxHeight-height)/2, offsetStart+width, (maxHeight-height)/2+height) + draw.BiLinear.Scale(result, dstBounds, img, img.Bounds(), draw.Src, nil) + } + return result, nil +} + +func splitRows(imagePaths []string, metadata []imageMetadata, width, rowWidth int) [][]string { + var rows [][]string + for len(imagePaths) > 0 { + row := imagePaths[:0] + thisRowWidth := 0 + for ; len(row) < len(imagePaths); row = row[:len(row)+1] { + nextW := max(width, runewidth.StringWidth(escapeString(filepath.Base(imagePaths[len(row)])))) + if *l { + nextW = max(nextW, len(metadata[len(row)].String())) + } + if len(row) > 0 { + nextW += 2 + } + if thisRowWidth > 0 && thisRowWidth+nextW > rowWidth { + break + } + thisRowWidth += nextW + } + imagePaths = imagePaths[len(row):] + if *l { + metadata = metadata[len(row):] + } + rows = append(rows, row) + } + return rows +} + +func thumbnailFiles(w io.Writer, imagePaths []string, width, rowWidth, cellX, cellY int) error { + padding := 2 + aspectRatio := pixelAspectRatio{1, 1} + switch *m { + case modeBlock: + aspectRatio = pixelAspectRatio{cellX, cellY} + cellX = 1 + case modeBlock24: + aspectRatio = pixelAspectRatio{2 * cellX, cellY} + cellX = 1 + case modeSixel: + padding *= cellX + } + var metadata []imageMetadata + if *l { + metadata = make([]imageMetadata, len(imagePaths)) + for i, filename := range imagePaths { + var err error + if metadata[i], err = readMetadata(filename); err != nil { + return err + } + } + } + rows := splitRows(imagePaths, metadata, width, rowWidth) + rowMetadata := make([][]imageMetadata, len(rows)) + if *l { + for i, row := range rows { + rowMetadata[i] = metadata[:len(row)] + metadata = metadata[len(row):] + } + } + rowImages := make([]chan image.Image, len(rows)) + for i := range rowImages { + rowImages[i] = make(chan image.Image, 1) + } + errCh := make(chan error, 1) + go func() { + sem := make(chan struct{}, 20) + for i, row := range rows { + sem <- struct{}{} + go func() { + defer func() { <-sem }() + img, err := compositeImageRow(row, rowMetadata[i], width*cellX, rowWidth*cellX, cellX, aspectRatio) + if err != nil { + select { + case errCh <- err: + default: + } + return + } + rowImages[i] <- img + }() + } + }() + for i, row := range rows { + var rowImage image.Image + select { + case err := <-errCh: + return err + case rowImage = <-rowImages[i]: + } + printImg(w, rowImage) + fmt.Fprintln(w) + layouts := layoutRow(row, rowMetadata[i], width*cellX, rowWidth*cellX, cellX) + realOffset := 0 + for j, filename := range row { + start := divRound(layouts[j], cellX) + end := rowWidth + if j+1 < len(row) { + end = divRound(layouts[j+1], cellX) + } + fmt.Fprintf(w, "%*s", start-realOffset, "") + realOffset = start + fileNameWidth := runewidth.StringWidth(escapeString(filepath.Base(filename))) + padding := max((end-start-fileNameWidth)/2, 0) + fmt.Fprintf(w, "%*s%s", padding, "", escapeString(filepath.Base(filename))) + realOffset += padding + fileNameWidth + } + fmt.Fprintln(w) + if *l { + realOffset = 0 + for j, metadata := range rowMetadata[i] { + start := divRound(layouts[j], cellX) + end := rowWidth + if j+1 < len(row) { + end = divRound(layouts[j+1], cellX) + } + fmt.Fprintf(w, "%*s", start-realOffset, "") + realOffset = start + details := metadata.String() + padding := max((end-start-len(details))/2, 0) + fmt.Fprintf(w, "%*s%s", padding, "", details) + realOffset += padding + len(details) + } + fmt.Fprintln(w) + } + } + return nil +} + +func readDir(d string) ([]string, error) { + files, err := os.ReadDir(d) + if err != nil { + return nil, err + } + filenames := make([]string, len(files)) + for i, f := range files { + filenames[i] = filepath.Join(d, f.Name()) + } + return filenames, nil +} + +func thumbnailDir(w io.Writer, dir string, width, rowWidth, cellX, cellY int) error { + files, err := readDir(dir) + if err != nil { + return err + } + files = slices.DeleteFunc(files, func(filename string) bool { return !isImage(filename) }) + return thumbnailFiles(w, files, width, rowWidth, cellX, cellY) +} + +func thumbnailer(args []string) error { + cols := *x + if *x <= 0 { + return fmt.Errorf("-x must be greater than 0") + } + ws, err := unix.IoctlGetWinsize(int(os.Stdout.Fd()), unix.TIOCGWINSZ) + if err != nil { + return fmt.Errorf("terminal size: %s", err) + } + termCols := int(ws.Col) + cellX, cellY := int(ws.Xpixel)/int(ws.Col), int(ws.Ypixel)/int(ws.Row) + + cols = min(cols, termCols) + + var files, dirs []string + if len(args) == 0 { + dirs = []string{"."} + } else { + for _, arg := range args { + if isDir(arg) { + dirs = append(dirs, arg) + } else { + files = append(files, arg) + } + } + } + w := bufio.NewWriter(os.Stdout) + defer w.Flush() + if err := thumbnailFiles(w, files, cols, termCols, cellX, cellY); err != nil { + return err + } + if len(files) > 0 && len(dirs) > 0 { + fmt.Fprintln(w) + } + for i, d := range dirs { + if i > 0 { + fmt.Fprintln(w) + } + if len(dirs) > 1 { + fmt.Fprintf(w, "%s:\n", d) + } + if err := thumbnailDir(w, d, cols, termCols, cellX, cellY); err != nil { + return err + } + } + + return nil +} + +func main() { + flag.Usage = func() { + fmt.Fprintf(os.Stderr, "Usage: thumbnailer [OPTION]... [FILE]...\n") + flag.PrintDefaults() + } + flag.Parse() + + if err := thumbnailer(flag.Args()); err != nil { + fmt.Fprintf(os.Stderr, "Failed: %s\n", err) + os.Exit(1) + } +} -- cgit v1.3.1