package main import ( _ "embed" "fmt" "log" "os" "path/filepath" "slices" "strconv" "strings" "git.yetaga.in/alazyreader/why/filetypes" "github.com/disintegration/imaging" tk "modernc.org/tk9.0" ) // this is a default image // //go:embed noise.png var noise []byte // this is the app icon // //go:embed icon.png var icon []byte //go:generate go run ./filetypes/cmd/gen.go var validFileTypes = filetypes.Valid var metaActive bool var fileList *tk.ToplevelWidget var lb *tk.ListboxWidget 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 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(img *tk.LabelWidget) func() { return func() { 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(img, file) } } func openFileAndDirectory(img *tk.LabelWidget, 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, img) } func newDirectory(img *tk.LabelWidget) func() { return func() { 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]), img) } } func updateImage(file string, img *tk.LabelWidget) { i, err := imaging.Open(file, imaging.AutoOrientation(true)) if err != nil { log.Printf("error opening image: %v", err) return } i = imaging.Fit(i, must(strconv.Atoi(tk.WinfoScreenWidth(tk.App))), must(strconv.Atoi(tk.WinfoScreenHeight(tk.App))), imaging.Linear, ) repaint(img, filepath.Base(file), tk.Data(i)) directoryState.currentFile = filepath.Base(file) } func repaint(img *tk.LabelWidget, name string, opt tk.Opt) { img.Configure(tk.Image(tk.NewPhoto(opt))) if name != "" { tk.App.WmTitle(fmt.Sprintf("why | %s", name)) } else { tk.App.WmTitle("why") } } func main() { img := tk.Label() repaint(img, "", tk.Data(noise)) tk.MacOpenDocument(func(e string) { openFileAndDirectory(img, e) }) menubar := tk.Menu() fileMenu := menubar.Menu() fileMenu.AddCommand(tk.Lbl("Open File"), tk.Accelerator("Meta+O"), tk.Command(newFileInDirectory(img))) fileMenu.AddCommand(tk.Lbl("Open Directory"), tk.Command(newDirectory(img))) fileMenu.AddSeparator() checkbox := fileMenu.AddCheckbutton(tk.Lbl("Show Filelist")) bindVar := tk.Variable("FileList") fileMenu.EntryConfigure(checkbox, bindVar) menubar.AddCascade(tk.Lbl("File"), tk.Underline(0), tk.Mnu(fileMenu)) tk.App.Configure(tk.Mnu(menubar)) destroyFileList := func(destroy bool) { if destroy { tk.Destroy(fileList) } fileList = nil lb = nil bindVar.Set("0") } constructFileList := func() { 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]]), img) })) tk.Bind(fileList, "", tk.Command(func(e *tk.Event) { // list closed by user click on destroyFileList(false) })) bindVar.Set("1") } tk.Bind(fileMenu, "<>", tk.Command(func() { if bindVar.Get() == "1" && fileList == nil { constructFileList() } else if bindVar.Get() == "0" && fileList != nil { destroyFileList(true) } })) // TODO: if someone presses the Meta key again after the openfile dialog box closes, // it triggers a _release_ event instead of a press event. The second time afterward, it works correctly. tk.Bind(tk.App, "", tk.Command(func(e *tk.Event) { curr := slices.Index(directoryState.images, directoryState.currentFile) switch e.Keysym { case ".": log.Printf("state: %+v", directoryState) case "Meta_L", "Meta_R": metaActive = true case "o": if metaActive { newFileInDirectory(img)() } case "a": if metaActive { if fileList == nil { constructFileList() } else { // list closed by Meta-a destroyFileList(true) } } case "Up": if curr > 0 { updateImage(filepath.Join(directoryState.currentDirectory, directoryState.images[curr-1]), img) if lb != nil { lb.SelectionClear("0", "end") lb.SelectionSet(curr - 1) } } case "Down": if curr < len(directoryState.images)-1 && curr != -1 { updateImage(filepath.Join(directoryState.currentDirectory, directoryState.images[curr+1]), img) if lb != nil { lb.SelectionClear("0", "end") lb.SelectionSet(curr + 1) } } } })) tk.Bind(tk.App, "", tk.Command(func(e *tk.Event) { switch e.Keysym { case "Meta_L", "Meta_R": metaActive = false } })) // 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)) // })) tk.Pack(img) tk.App.IconPhoto(tk.NewPhoto(tk.Data(icon))) tk.App.Wait() }