diff --git a/kittens/iv/Makefile b/kittens/iv/Makefile new file mode 100644 index 00000000000..c86d9c6fbe5 --- /dev/null +++ b/kittens/iv/Makefile @@ -0,0 +1,2 @@ +all: + go build main.go diff --git a/kittens/iv/Pipfile b/kittens/iv/Pipfile new file mode 100644 index 00000000000..645a67eae77 --- /dev/null +++ b/kittens/iv/Pipfile @@ -0,0 +1,11 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] + +[dev-packages] + +[requires] +python_version = "3.12" diff --git a/kittens/iv/Pipfile.lock b/kittens/iv/Pipfile.lock new file mode 100644 index 00000000000..b6df5da034d --- /dev/null +++ b/kittens/iv/Pipfile.lock @@ -0,0 +1,20 @@ +{ + "_meta": { + "hash": { + "sha256": "702ad05de9bc9de99a4807c8dde1686f31e0041d7b5f6f6b74861195a52110f5" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.12" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": {}, + "develop": {} +} diff --git a/kittens/iv/config.yaml b/kittens/iv/config.yaml new file mode 100644 index 00000000000..c4bed2f155a --- /dev/null +++ b/kittens/iv/config.yaml @@ -0,0 +1,4 @@ +windowParam: + xParam: 3 + yParam: 2 + diff --git a/kittens/iv/go.mod b/kittens/iv/go.mod new file mode 100644 index 00000000000..81e5fc031fb --- /dev/null +++ b/kittens/iv/go.mod @@ -0,0 +1,15 @@ +module gkitten + +go 1.22.3 + +require ( + github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect + github.com/spf13/cobra v1.8.1 // indirect + github.com/spf13/pflag v1.0.5 // indirect + golang.org/x/sys v0.22.0 // indirect + golang.org/x/term v0.22.0 // indirect + gonum.org/v1/gonum v0.15.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) diff --git a/kittens/iv/go.sum b/kittens/iv/go.sum new file mode 100644 index 00000000000..c60779a818a --- /dev/null +++ b/kittens/iv/go.sum @@ -0,0 +1,22 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 h1:XBBHcIb256gUJtLmY22n99HaZTz+r2Z51xUPi01m3wg= +github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203/go.mod h1:E1jcSv8FaEny+OP/5k9UxZVw9YFWGj7eI4KR/iOBqCg= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= +golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= +gonum.org/v1/gonum v0.15.0 h1:2lYxjRbTYyxkJxlhC+LvJIx3SsANPdRybu1tGj9/OrQ= +gonum.org/v1/gonum v0.15.0/go.mod h1:xzZVBJBtS+Mz4q0Yl2LJTk+OxOg4jiXZ7qBoM0uISGo= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/kittens/iv/ls b/kittens/iv/ls new file mode 100644 index 00000000000..e69de29bb2d diff --git a/kittens/iv/main b/kittens/iv/main new file mode 100755 index 00000000000..5915322fe65 Binary files /dev/null and b/kittens/iv/main differ diff --git a/kittens/iv/main.go b/kittens/iv/main.go new file mode 100644 index 00000000000..4579043c478 --- /dev/null +++ b/kittens/iv/main.go @@ -0,0 +1,619 @@ +package main + +import ( + "encoding/base64" + "fmt" + "image" + _ "image/jpeg" + "image/png" + _ "image/png" + "io/fs" + "log" + "os" + "os/signal" + "path/filepath" + "strings" + "sync" + "syscall" + "time" + + // "kitty/tools/cli" + + "github.com/eiannone/keyboard" + "github.com/nfnt/resize" + "github.com/spf13/cobra" + "golang.org/x/sys/unix" + "gonum.org/v1/gonum/mat" + "gopkg.in/yaml.v2" +) + +func init() { + globalImageCoordinates = make(map[string][2]int) +} + +var rootCmd = &cobra.Command{ + Use: "icat [directory]", + Short: "Kitten to display images in a grid layout in Kitty terminal", + Run: session, +} + +// Will contain all the window parameters +type windowParameters struct { + Row uint16 + Col uint16 + xPixel uint16 + yPixel uint16 +} + +// Will contain Global Navigation +type navigationParameters struct { + imageIndex int // Index of the Image selected as per globalImages + x int // Horizontal Grid Coordinate + y int // Vertical Grid Coordinate +} + +// To hold info the image , it's height and width +type ImageInfo struct { + Path string + Width int + Height int +} + +// For matrix calculations ,could be useful for more complex image manipulations in the future. +type Matrix2D struct { + mat *mat.Dense +} + +type Config struct { + GridParam struct { + XParam int `yaml:"x_param"` + YParam int `yaml:"y_param"` + } `yaml:"grid_param"` +} + +type ImageBound struct { + xBound int + yBound int +} + +type ImageCoordinates map[string][2]int + +var ( + globalWindowParameters windowParameters // Contains Global Level Window Parameters + globalConfig Config + globalNavigation navigationParameters + globalImages []string + globalImagePages [][]string + globalImageBound ImageBound + globalImageCoordinates ImageCoordinates +) + +func xyToIndex(x, y, x_param int) int { + return y*x_param + x +} + +func indexToXY(index, x_param int) (int, int) { + y := index / x_param + x := index % x_param + return x, y +} + + +/// NOT NEEDED SINCE USING RESIZE METHOD +// // creates a new 2D matrix +// func NewMatrix2D() *Matrix2D { +// return &Matrix2D{mat: mat.NewDense(3, 3, nil)} +// } + +// // This will set the scale factors for the matrix for transformation later +// func (m *Matrix2D) Scale(sx, sy float64) { +// m.mat.Set(0, 0, sx) +// m.mat.Set(1, 1, sy) +// m.mat.Set(2, 2, 1) +// } + +// //This will set the translation factors for the matrix for transformation later +// func (m *Matrix2D) Translate(tx, ty float64) { +// m.mat.Set(0, 2, tx) +// m.mat.Set(1, 2, ty) +// } + +// // applies the transformation to a point +// func (m *Matrix2D) Transform(x, y float64) (float64, float64) { +// point := mat.NewDense(3, 1, []float64{x, y, 1}) +// result := mat.NewDense(3, 1, nil) +// result.Mul(m.mat, point) +// return result.At(0, 0), result.At(1, 0) +// } + + +//========= CODE FOR FINDING COORDS OF AN IMAGE IN THE GRID (AS PER IT's RESOLUTION)========= + +// To retrieve the dimensions of an image +func GetImageInfo(path string) (ImageInfo, error) { + file, err := os.Open(path) + if err != nil { + return ImageInfo{}, err + } + defer file.Close() + + img, _, err := image.DecodeConfig(file) + if err != nil { + return ImageInfo{}, err + } + + return ImageInfo{Path: path, Width: img.Width, Height: img.Height}, nil +} + + +//determines if an image fits in the grid and scales if necessary +func FitImageToGrid(img ImageInfo, gridWidth, gridHeight int) (int, int, bool) { + if img.Width <= gridWidth && img.Height <= gridHeight { + return img.Width, img.Height, true + } + + aspectRatio := float64(img.Width) / float64(img.Height) + gridRatio := float64(gridWidth) / float64(gridHeight) + + var newWidth, newHeight int + if aspectRatio > gridRatio { + newWidth = gridWidth + newHeight = int(float64(gridWidth) / aspectRatio) + } else { + newHeight = gridHeight + newWidth = int(float64(gridHeight) * aspectRatio) + } + + return newWidth, newHeight, false +} + +// calculates the position to center an image in a grid cell +func CenterImageInGrid(imgWidth, imgHeight, gridWidth, gridHeight int) (int, int) { + x := (gridWidth - imgWidth) / 2 + y := (gridHeight - imgHeight) / 2 + return x, y +} + + +// scales an image to fit the grid and centers it +func ScaleAndCenterImage(img image.Image, gridWidth, gridHeight int) (image.Image, int, int) { + bounds := img.Bounds() + imgWidth, imgHeight := bounds.Dx(), bounds.Dy() + + newWidth, newHeight, fits := FitImageToGrid(ImageInfo{Width: imgWidth, Height: imgHeight}, gridWidth, gridHeight) + + var scaledImg image.Image + if !fits { + // Use 0 for one of the dimensions to maintain aspect ratio + if float64(newWidth)/float64(imgWidth) < float64(newHeight)/float64(imgHeight) { + scaledImg = resize.Resize(uint(newWidth), 0, img, resize.Lanczos3) + } else { + scaledImg = resize.Resize(0, uint(newHeight), img, resize.Lanczos3) + } + } else { + scaledImg = img + } + + // Recalculate dimensions after scaling + scaledBounds := scaledImg.Bounds() + finalWidth, finalHeight := scaledBounds.Dx(), scaledBounds.Dy() + + x, y := CenterImageInGrid(finalWidth, finalHeight, gridWidth, gridHeight) + return scaledImg, x, y +} + +//Converts stored coordinates to actual grid positions + +// Example calculation: +// Let's assume we have a 3x2 grid layout (3 columns, 2 rows) in a terminal window of 600x400 pixels. +// Our globalImageBound would be calculated as: +// xBound = 600 / 3 = 200 pixels (width of each grid cell) +// yBound = 400 / 2 = 200 pixels (height of each grid cell) +// +// Now, let's convert the stored coordinate (1, 1) to its actual position: +// +// Input: +// x = 1, y = 1 +// globalImageBound.xBound = 200, globalImageBound.yBound = 200 +// +// Calculation: +// actualX = x * globalImageBound.xBound = 1 * 200 = 200 +// actualY = y * globalImageBound.yBound = 1 * 200 = 200 +// +// Result: +// The actual top-left corner of the grid cell for (1, 1) is (200, 200) in pixel coordinates. +// +// This means: +// - (0, 0) would be at (0, 0) in pixels +// - (1, 0) would be at (200, 0) in pixels +// - (2, 0) would be at (400, 0) in pixels +// - (0, 1) would be at (0, 200) in pixels +// - (1, 1) would be at (200, 200) in pixels +// - (2, 1) would be at (400, 200) in pixels + +func ConvertToActualGridPosition(x, y int, globalImageBound ImageBound) (int, int) { + actualX := x * globalImageBound.xBound + actualY := y * globalImageBound.yBound + return actualX, actualY +} + + +// main function to handle image placement +func PlaceImageInGrid(imagePath string, x, y int, globalImageBound ImageBound, globalWindowParameters windowParameters) (image.Image, int, int, error) { + // Convert stored coordinates to actual grid positions + actualX, actualY := ConvertToActualGridPosition(x, y, globalImageBound) + + // Get image info + _, err := GetImageInfo(imagePath) + if err != nil { + return nil, 0, 0, err + } + + // Calculate grid cell dimensions + gridWidth := int(globalWindowParameters.xPixel) / globalConfig.GridParam.XParam + gridHeight := int(globalWindowParameters.yPixel) / globalConfig.GridParam.YParam + + // Load the image to get its info + file, err := os.Open(imagePath) + if err != nil { + return nil, 0, 0, err + } + defer file.Close() + + img, _, err := image.Decode(file) + if err != nil { + return nil, 0, 0, err + } + + // Scale and center the image + scaledImg, offsetX, offsetY := ScaleAndCenterImage(img, gridWidth, gridHeight) + + // Calculate the final position using actual grid positions + finalX := actualX*gridWidth + offsetX + finalY := actualY*gridHeight + offsetY + + return scaledImg, -finalX, -finalY, nil +} + +func debugPrintImage(imagePath string, width, height, x, y int) { + fmt.Printf("Debug: Image %s (size: %dx%d) placed at position (%d, %d)\n", imagePath, width, height, x, y) +} + +// Assign Coordinates to each Image in a Page +//func pageCoordinater() { +// globalImageBound.xBound = int(globalWindowParameters.Row) / globalConfig.GridParam.XParam +// globalImageBound.yBound = int(globalWindowParameters.Col) / globalConfig.GridParam.YParam +// +// for imageIndex, imagePath := range globalImages { +// x, y := indexToXY(imageIndex, globalConfig.GridParam.XParam) +// coordinates := [2]int{x, y} +// globalImageCoordinates[imagePath] = coordinates +// } +// +// fmt.Println(globalImageCoordinates) +//} + + +func pageCoordinater() { + + globalImageBound.xBound = int(globalWindowParameters.Row) / globalConfig.GridParam.XParam + globalImageBound.yBound = int(globalWindowParameters.Col) / globalConfig.GridParam.YParam + + fmt.Printf("xBound: %d, yBound: %d\n", globalImageBound.xBound, globalImageBound.yBound) + fmt.Printf("Number of images: %d\n", len(globalImages)) + + for imageIndex, imagePath := range globalImages { + x, y := indexToXY(imageIndex, globalConfig.GridParam.XParam) + coordinates := [2]int{globalImageBound.xBound * x, globalImageBound.yBound * y} + globalImageCoordinates[imagePath] = coordinates + } + + fmt.Println("Final globalImageCoordinates:") + for path, coord := range globalImageCoordinates { + fmt.Printf("%s: (%d, %d)\n", path, coord[0], coord[1]) + } +} + +func pageCoordinater2() { + + fmt.Printf("globalWindowParameters.Row: %d\n", globalWindowParameters.Row) + fmt.Printf("globalWindowParameters.Col: %d\n", globalWindowParameters.Col) + fmt.Printf("globalConfig.GridParam.XParam: %d\n", globalConfig.GridParam.XParam) + fmt.Printf("globalConfig.GridParam.YParam: %d\n", globalConfig.GridParam.YParam) + + globalImageBound.xBound = int(globalWindowParameters.Row) / globalConfig.GridParam.XParam + globalImageBound.yBound = int(globalWindowParameters.Col) / globalConfig.GridParam.YParam + + fmt.Printf("xBound: %d, yBound: %d\n", globalImageBound.xBound, globalImageBound.yBound) + fmt.Printf("Number of images: %d\n", len(globalImages)) + fmt.Printf("XParam: %d\n", globalConfig.GridParam.XParam) + + for imageIndex, imagePath := range globalImages { + x, y := indexToXY(imageIndex, globalConfig.GridParam.XParam) + fmt.Printf("Before multiplication - imageIndex: %d, x: %d, y: %d\n", imageIndex, x, y) + + coordinates := [2]int{globalImageBound.xBound * x, globalImageBound.yBound * y} + globalImageCoordinates[imagePath] = coordinates + + fmt.Printf("After multiplication - imagePath: %s, coordinates: (%d, %d)\n", imagePath, coordinates[0], coordinates[1]) + } + + fmt.Println("Final globalImageCoordinates:") + for path, coord := range globalImageCoordinates { + fmt.Printf("%s: (%d, %d)\n", path, coord[0], coord[1]) + } +} + +// This function takes globalConfig struct and parses the YAML data +func loadConfig(filename string) error { + data, err := os.ReadFile(filename) + if err != nil { + log.Printf("Error reading file: %v", err) + return err + } + + err = yaml.Unmarshal(data, &globalConfig) + if err != nil { + log.Printf("Error unmarshaling YAML: %v", err) + return err + } + + return nil +} + +// Gets the window size and modifies the globalWindowParameters (global struct) +func getWindowSize(window windowParameters) error { + var err error + var f *os.File + + // Read the window size from device drivers and print them + if f, err = os.OpenFile("/dev/tty", unix.O_NOCTTY|unix.O_CLOEXEC|unix.O_NDELAY|unix.O_RDWR, 0666); err == nil { + var sz *unix.Winsize + if sz, err = unix.IoctlGetWinsize(int(f.Fd()), unix.TIOCGWINSZ); err == nil { + fmt.Printf("rows: %v columns: %v width: %v height %v\n", sz.Row, sz.Col, sz.Xpixel, sz.Ypixel) + window.Row = sz.Row + window.Col = sz.Col + window.xPixel = sz.Xpixel + window.yPixel = sz.Ypixel + return nil + } + } + + fmt.Fprintln(os.Stderr, err) + // os.Exit(1) + + return err +} + +// Function handler for changes in window sizes (will be added to goroutines) +func handleWindowSizeChange() { + err := getWindowSize(globalWindowParameters) + if err != nil { + fmt.Println("Error getting window size:", err) + } +} + +// Checks if a given file is an image +func isImage(fileName string) bool { + extensions := []string{".jpg", ".jpeg", ".png", ".gif", ".bmp"} + ext := strings.ToLower(filepath.Ext(fileName)) + for _, e := range extensions { + if ext == e { + return true + } + } + return false +} + +// findImages recursively searches for image files in the given directory +func discoverImages(dir string) error { + err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if !d.IsDir() && isImage(d.Name()) { + globalImages = append(globalImages, path) + } + return nil + }) + if err != nil { + return fmt.Errorf("error walking directory: %w", err) + } + return nil +} + +// Resizes images, return +func resizeImage(img image.Image, width, height uint) image.Image { + return resize.Resize(width, height, img, resize.Lanczos3) +} + +func imageToBase64(img image.Image) (string, error) { + var buf strings.Builder + err := png.Encode(&buf, img) + if err != nil { + return "", err + } + encoded := base64.StdEncoding.EncodeToString([]byte(buf.String())) + return encoded, nil +} + +func loadImage(filePath string) (image.Image, error) { + file, err := os.Open(filePath) + if err != nil { + return nil, err + } + defer file.Close() + + img, _, err := image.Decode(file) + if err != nil { + return nil, err + } + return img, nil +} + +// Print image to Kitty Terminal +func printImageToKitty(encoded string, width, height int) { + fmt.Printf("\x1b_Gf=1,t=%d,%d;x=%s\x1b\\", width, height, encoded) +} + +func readKeyboardInput(navParams *navigationParameters, wg *sync.WaitGroup) { + defer wg.Done() + + // Open the keyboard + if err := keyboard.Open(); err != nil { + log.Fatal(err) + } + defer keyboard.Close() + + fmt.Println("Press 'h' to increment x, 'l' to decrement x, 'j' to increment y, 'k' to decrement y.") + fmt.Println("Press 'Ctrl+C' to exit.") + + for { + // Read the key event + char, key, err := keyboard.GetSingleKey() + if err != nil { + log.Fatal(err) + } + + // Handle the key event + switch char { + case 'h': + if (navParams.x > 0) { + navParams.x-- + } + case 'l': + navParams.x++ + case 'j': + navParams.y++ + case 'k': + if (navParams.y > 0) { // cursor is at the top most part of the screen + navParams.y-- + } + } + + // Update the image index which locates the image index to + navParams.imageIndex = xyToIndex(navParams.x, navParams.y, globalConfig.GridParam.XParam) + + // Print the current state of navigation parameters + fmt.Printf("Current navigation parameters (in goroutine): %+v\n", *navParams) + + // Exit the loop if 'Ctrl+C' is pressed + if key == keyboard.KeyCtrlC { + break + } + } + + var xParam int = globalConfig.GridParam.XParam + var yParam int = globalConfig.GridParam.YParam + + for i := 0; i < len(globalImages); i += xParam { + end := i + xParam + if end > len(globalImages) { + end = len(globalImages) + } + + row := globalImages[i:end] + globalImagePages = append(globalImagePages, row) + + if len(globalImagePages) == yParam { + break + } + } +} + +// Routine for session - kitten will run in this space +func session(cmd *cobra.Command, args []string) { + + // Check for Arguements + if len(args) == 0 { + fmt.Println("Please specify a directory") + os.Exit(1) + } + + // Get directory name and discover images + dir := args[0] + err := discoverImages(dir) + if err != nil { + fmt.Printf("Error discovering images: %v\n", err) + os.Exit(1) + } + + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, syscall.SIGWINCH) + + // Get the window size initially when kitten is spawned + handleWindowSizeChange() + + // Goroutine to listen for window size changes + go func() { + for { + sig := <-sigs + + // if window size change syscall is detected, execute the handleWindowSizeChange() + if sig == syscall.SIGWINCH { + handleWindowSizeChange() + } + } + }() + + /* Getting Keyboard Inputs into Goroutines + Here, the keyboard handler with keep updating the globalNavigation and update x and y. + globalNavigation contains all the global cooridinates, updated regularly and keeps the whole + program aware of current state of keyboard. + */ + + var keyboardWg sync.WaitGroup + keyboardWg.Add(1) + + go readKeyboardInput(&globalNavigation, &keyboardWg) + + // Till this point, WindowSize Changes would be handled and stored into globalWindowParameters + + /* Load system configuration from kitty.conf + Currently, the loadConfig is loading configurations from config.yaml, parsing can be updated later + */ + err = loadConfig("./config.yaml") + if (err != nil) { + fmt.Printf("Error Parsing config file, exiting ....") + os.Exit(1) + } + + // globalConfig.GridParam.XParam = 3 + // globalConfig.GridParam.YParam = 2 + fmt.Printf("Window parameters: X = %d, Y = %d\n", + globalConfig.GridParam.XParam, + globalConfig.GridParam.YParam) + + // if x_param or y_param are 0, exit + if (globalConfig.GridParam.XParam == 0 || globalConfig.GridParam.YParam == 0) { + fmt.Printf("x_param or y_param set to 0, check the system config file for kitty") + os.Exit(1) + } + + + pageCoordinater() + + + for imagePath, coordinates := range globalImageCoordinates { + x, y := coordinates[0], coordinates[1] + img, posX, posY, err := PlaceImageInGrid(imagePath, x, y, globalImageBound, globalWindowParameters) + if err != nil { + fmt.Printf("Error processing image %s: %v\n", imagePath, err) + continue + } + bounds := img.Bounds() + debugPrintImage(imagePath, bounds.Dx(), bounds.Dy(), posX, posY) + } + + + time.Sleep(100 * time.Second) + +} + +func main() { + + if err := rootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} diff --git a/kittens/iv/main.py b/kittens/iv/main.py new file mode 100644 index 00000000000..a9f57460a33 --- /dev/null +++ b/kittens/iv/main.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python + +import sys +from kitty.cli import parse_args + +OPTIONS = '''\ +--directory -d +type=str +help=Directory to process +''' + +def main(args): + try: + opts = parse_args(args[1:], OPTIONS, usage, help_text, 'directory_processor') + directory = opts.directory or (args[1] if len(args) > 1 else '.') + print(f"Processing directory: {directory}") + # Add your directory processing logic here + except Exception as e: + print(f"An error occurred: {e}", file=sys.stderr) + return 1 + return 0 + +if __name__ == '__main__': + sys.exit(main(sys.argv)) diff --git a/kittens/iv/main_test.go b/kittens/iv/main_test.go new file mode 100644 index 00000000000..e43f2060cc6 --- /dev/null +++ b/kittens/iv/main_test.go @@ -0,0 +1,225 @@ +package main + +import ( + "encoding/base64" + "image" + "image/color" + "image/png" + "os" + "path/filepath" + "strings" + "testing" +) + +// Testing for xyToIndex function +func TestXyToIndex(t *testing.T) { + x, y, x_param := 1, 2, 3 + expected := 7 + if result := xyToIndex(x, y, x_param); result != expected { + t.Errorf("xyToIndex(%d, %d, %d) = %d; want %d", x, y, x_param, result, expected) + } +} + +// Testing for indexToXY function +func TestIndexToXY(t *testing.T) { + index, x_param := 7, 3 + expectedX, expectedY := 1, 2 + x, y := indexToXY(index, x_param) + if x != expectedX || y != expectedY { + t.Errorf("indexToXY(%d, %d) = (%d, %d); want (%d, %d)", index, x_param, x, y, expectedX, expectedY) + } +} + +// Testing for loadConfig function +func TestLoadConfig(t *testing.T) { + configData := ` +grid_param: + x_param: 3 + y_param: 2 +` + tmpFile, err := os.CreateTemp("", "config*.yaml") + if err != nil { + t.Fatalf("Failed to create temp file: %v", err) + } + defer os.Remove(tmpFile.Name()) + + if _, err := tmpFile.Write([]byte(configData)); err != nil { + t.Fatalf("Failed to write to temp file: %v", err) + } + if err := tmpFile.Close(); err != nil { + t.Fatalf("Failed to close temp file: %v", err) + } + + err = loadConfig(tmpFile.Name()) + if err != nil { + t.Errorf("loadConfig() returned error: %v", err) + } + if globalConfig.GridParam.XParam != 3 || globalConfig.GridParam.YParam != 2 { + t.Errorf("loadConfig() loaded unexpected values: %+v", globalConfig.GridParam) + } +} + +// Testing for isImage function +func TestIsImage(t *testing.T) { + validImages := []string{"image.jpg", "photo.png", "pic.gif"} + invalidImages := []string{"document.txt", "music.mp3", "video.mp4"} + + for _, fileName := range validImages { + if !isImage(fileName) { + t.Errorf("isImage(%q) = false; want true", fileName) + } + } + + for _, fileName := range invalidImages { + if isImage(fileName) { + t.Errorf("isImage(%q) = true; want false", fileName) + } + } +} + +// Testing for discoverImages function +func TestDiscoverImages(t *testing.T) { + testDir := t.TempDir() + imgFile := filepath.Join(testDir, "image.jpg") + os.WriteFile(imgFile, []byte{}, 0644) + + err := discoverImages(testDir) + if err != nil { + t.Errorf("discoverImages() returned error: %v", err) + } + if len(globalImages) != 1 || globalImages[0] != imgFile { + t.Errorf("discoverImages() = %v; want [%v]", globalImages, imgFile) + } +} + +// Testing for resizeImage function +func TestResizeImage(t *testing.T) { + img := image.NewRGBA(image.Rect(0, 0, 100, 100)) + width, height := uint(50), uint(50) + resized := resizeImage(img, width, height) + if resized.Bounds().Dx() != int(width) || resized.Bounds().Dy() != int(height) { + t.Errorf("resizeImage() = %v; want width %d and height %d", resized.Bounds(), width, height) + } +} + +// Testing for imageToBase64 function +func TestImageToBase64(t *testing.T) { + img := image.NewRGBA(image.Rect(0, 0, 1, 1)) + img.Set(0, 0, color.RGBA{255, 0, 0, 255}) + + base64Str, err := imageToBase64(img) + if err != nil { + t.Errorf("imageToBase64() returned error: %v", err) + } + + decoded, err := base64.StdEncoding.DecodeString(base64Str) + if err != nil { + t.Errorf("Failed to decode base64 string: %v", err) + } + + decodedImg, err := png.Decode(strings.NewReader(string(decoded))) + if err != nil { + t.Errorf("Failed to decode image from base64 string: %v", err) + } + + if decodedImg.Bounds() != img.Bounds() { + t.Errorf("Decoded image bounds = %v; want %v", decodedImg.Bounds(), img.Bounds()) + } +} + +// Testing for loadImage function +func TestLoadImage(t *testing.T) { + img := image.NewRGBA(image.Rect(0, 0, 1, 1)) + tmpFile, err := os.CreateTemp("", "image*.png") + if err != nil { + t.Fatalf("Failed to create temp file: %v", err) + } + defer os.Remove(tmpFile.Name()) + + if err := png.Encode(tmpFile, img); err != nil { + t.Fatalf("Failed to encode image to temp file: %v", err) + } + if err := tmpFile.Close(); err != nil { + t.Fatalf("Failed to close temp file: %v", err) + } + + loadedImg, err := loadImage(tmpFile.Name()) + if err != nil { + t.Errorf("loadImage() returned error: %v", err) + } + if loadedImg.Bounds() != img.Bounds() { + t.Errorf("loadImage() = %v; want %v", loadedImg.Bounds(), img.Bounds()) + } +} + +// Testing for getWindowSize function +func TestGetWindowSize(t *testing.T) { + var window windowParameters + err := getWindowSize(window) + if err != nil { + t.Errorf("getWindowSize() returned error: %v", err) + } +} + +// Testing for printImageToKitty function +func TestPrintImageToKitty(t *testing.T) { + img := image.NewRGBA(image.Rect(0, 0, 1, 1)) + img.Set(0, 0, color.RGBA{255, 0, 0, 255}) + + base64Str, err := imageToBase64(img) + if err != nil { + t.Fatalf("Failed to convert image to base64: %v", err) + } + + printImageToKitty(base64Str, 1, 1) +} + +// Testing for pageCoordinater function +func TestPageCoordinater(t *testing.T) { + globalConfig.GridParam.XParam = 2 + globalConfig.GridParam.YParam = 2 + globalWindowParameters.Row = 20 + globalWindowParameters.Col = 20 + globalImages = []string{"img1", "img2", "img3", "img4"} + + pageCoordinater() + + expectedCoordinates := map[string][2]int{ + "img1": {0, 0}, + "img2": {1, 0}, + "img3": {0, 1}, + "img4": {1, 1}, + } + + for img, coord := range expectedCoordinates { + if globalImageCoordinates[img] != coord { + t.Errorf("Coordinates for %s = %v; want %v", img, globalImageCoordinates[img], coord) + } + } +} + +// Testing for paginateImages function +func TestPaginateImages(t *testing.T) { + globalConfig.GridParam.XParam = 2 + globalConfig.GridParam.YParam = 2 + globalImages = []string{"img1", "img2", "img3", "img4", "img5"} + + paginateImages() + + expectedPages := [][]string{ + {"img1", "img2"}, + {"img3", "img4"}, + } + + if len(globalImagePages) != len(expectedPages) { + t.Errorf("Number of pages = %d; want %d", len(globalImagePages), len(expectedPages)) + } + + for i, page := range expectedPages { + for j, img := range page { + if globalImagePages[i][j] != img { + t.Errorf("Page %d, Image %d = %s; want %s", i, j, globalImagePages[i][j], img) + } + } + } +}