package main import ( _ "embed" "fmt" "image" "log" "os" "path/filepath" "slices" "strconv" "strings" "git.yetaga.in/alazyreader/why/filetypes" "github.com/dblezek/tga" "github.com/disintegration/imaging" "golang.org/x/image/webp" tk "modernc.org/tk9.0" ) //go:embed noise.png var noise []byte // this is a default image //go:embed icon.png var icon []byte // this is the app icon //go:generate go run ./filetypes/cmd/gen.go var validFileTypes = filetypes.Valid var fileList *tk.ToplevelWidget var lb *tk.ListboxWidget var img = tk.Label() var fileListBindVar = tk.Variable("FileList") var directoryState struct { currentDirectory string currentFile string images []string } func must[T any](t T, err error) T { if err != nil { panic(err) } return t } func checkErr[T any](_ T, err error) bool { return err == nil } func isImage(entry os.DirEntry) bool { if entry.IsDir() { return false } ext := filepath.Ext(entry.Name()) if ext == "" { return false } for _, ft := range validFileTypes { if slices.Contains(ft.MacExtensions, ext[1:]) { return true } } return false } func newFileInDirectory() { files := tk.GetOpenFile(tk.Filetypes(filetypes.GetTkTypes(validFileTypes)), tk.Multiple(false)) if len(files) < 1 || files[0] == "" { log.Println("no file chosen") return } file := strings.Join(files, " ") // GetOpenFile returns an array split on spaces! openFileAndDirectory(file) } func openFileAndDirectory(file string) { directoryState.currentFile = filepath.Base(file) directoryState.currentDirectory = filepath.Dir(file) dirfiles, err := os.ReadDir(directoryState.currentDirectory) if err != nil { log.Printf("could not open dir: %v", err) return } directoryState.images = []string{} for _, v := range dirfiles { if isImage(v) { directoryState.images = append(directoryState.images, v.Name()) } } updateImage(file) } func newDirectory() { dir := tk.ChooseDirectory() if dir == "" { log.Println("no directory chosen") return } directoryState.currentDirectory = dir dirfiles, err := os.ReadDir(directoryState.currentDirectory) if err != nil { log.Println(err) return } directoryState.images = []string{} for _, v := range dirfiles { if isImage(v) { directoryState.images = append(directoryState.images, v.Name()) } } updateImage(filepath.Join(directoryState.currentDirectory, directoryState.images[0])) } func updateImage(file string) { var i image.Image f, err := os.Open(file) if err != nil { log.Printf("error opening image: %v", err) return } defer f.Close() ext := strings.ToLower(strings.TrimPrefix(filepath.Ext(file), ".")) if checkErr(imaging.FormatFromExtension(ext)) { i, err = imaging.Decode(f, imaging.AutoOrientation(true)) } else if ext == "webp" { i, err = webp.Decode(f) } else if ext == "tga" { i, err = tga.Decode(f) } if err != nil { log.Printf("error decoding image: %v", err) return } i = imaging.Fit(i, // -50 to give some space to breathe around the edges must(strconv.Atoi(tk.WinfoScreenWidth(tk.App)))-50, must(strconv.Atoi(tk.WinfoScreenHeight(tk.App)))-50, imaging.CatmullRom, ) repaint(filepath.Base(file), tk.Data(i)) directoryState.currentFile = filepath.Base(file) } func repaint(name string, data tk.Opt) { // TODO: sometimes, when going from a big image to a smaller one, // the window remains the same height as the big image, // even as the width shrinks to fit the smaller image. // This only seems to happen when the large image was full-screen height. img.Configure(tk.Image(tk.NewPhoto(data))) if name != "" { tk.App.WmTitle(fmt.Sprintf("why | %s", name)) } else { tk.App.WmTitle("why") } tk.App.Center() } func destroyFileList(destroy bool) { if destroy { tk.Destroy(fileList) } fileList = nil lb = nil fileListBindVar.Set("0") } func constructFileList() { fileList = tk.Toplevel() fileList.WmTitle("Files") lb = fileList.Listbox(tk.Height(0)) for i := range directoryState.images { lb.Insert("end", directoryState.images[i]) if i == slices.Index(directoryState.images, directoryState.currentFile) { lb.SelectionSet(i) } } tk.Pack(lb) tk.Bind(lb, "<>", tk.Command(func() { selection := lb.Curselection() updateImage(filepath.Join(directoryState.currentDirectory, directoryState.images[selection[0]])) })) tk.Bind(fileList, "", tk.Command(func(e *tk.Event) { // list closed by user click on destroyFileList(false) })) fileListBindVar.Set("1") } func menuSelect() { if fileListBindVar.Get() == "1" && fileList == nil { constructFileList() } else if fileListBindVar.Get() == "0" && fileList != nil { destroyFileList(true) } } func moveSelectInFileList(target int) { if lb != nil { lb.SelectionClear("0", "end") lb.SelectionSet(target) lb.See(target) } } func keyPress(e *tk.Event) { curr := slices.Index(directoryState.images, directoryState.currentFile) switch e.Keysym { case ".": log.Printf("state: %+v", directoryState) case "o": newFileInDirectory() case "a": if fileList == nil { constructFileList() } else { destroyFileList(true) } case "Up", "Right": if curr > 0 { updateImage(filepath.Join(directoryState.currentDirectory, directoryState.images[curr-1])) moveSelectInFileList(curr - 1) } case "Down", "Left": if curr < len(directoryState.images)-1 && curr != -1 { updateImage(filepath.Join(directoryState.currentDirectory, directoryState.images[curr+1])) moveSelectInFileList(curr + 1) } } } func main() { tk.App.IconPhoto(tk.NewPhoto(tk.Data(icon))) tk.MacOpenDocument(openFileAndDirectory) menubar := tk.Menu() fileMenu := menubar.Menu() fileMenu.AddCommand(tk.Lbl("Open File"), tk.Accelerator("O"), tk.Command(newFileInDirectory)) fileMenu.AddCommand(tk.Lbl("Open Directory"), tk.Command(newDirectory)) fileMenu.AddSeparator() checkbox := fileMenu.AddCheckbutton(tk.Lbl("Show Filelist")) fileMenu.EntryConfigure(checkbox, fileListBindVar) menubar.AddCascade(tk.Lbl("File"), tk.Underline(0), tk.Mnu(fileMenu)) tk.App.Configure(tk.Mnu(menubar)) tk.Bind(fileMenu, "<>", tk.Command(menuSelect)) tk.Bind(tk.App, "", tk.Command(keyPress)) // todo: resize image based on scroll events // tk.Bind(tk.App, "", tk.Command(func(e *tk.Event) { // log.Printf("%v, %v", int16(e.Delta>>16), int16(e.Delta&0xFFFF)) // })) repaint("", tk.Data(noise)) tk.Pack(img) tk.App.Center() tk.App.Wait() }