Files
why/main.go

264 lines
6.6 KiB
Go

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 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(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) {
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(img, filepath.Base(file), tk.Data(i))
directoryState.currentFile = filepath.Base(file)
}
func repaint(img *tk.LabelWidget, 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 main() {
tk.App.IconPhoto(tk.NewPhoto(tk.Data(icon)), tk.DefaultIcon())
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("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, "<<ListboxSelect>>", tk.Command(func() {
selection := lb.Curselection()
updateImage(filepath.Join(directoryState.currentDirectory, directoryState.images[selection[0]]), img)
}))
tk.Bind(fileList, "<Destroy>", tk.Command(func(e *tk.Event) {
// list closed by user click on <x>
destroyFileList(false)
}))
bindVar.Set("1")
}
tk.Bind(fileMenu, "<<MenuSelect>>", tk.Command(func() {
if bindVar.Get() == "1" && fileList == nil {
constructFileList()
} else if bindVar.Get() == "0" && fileList != nil {
destroyFileList(true)
}
}))
tk.Bind(tk.App, "<KeyPress>", tk.Command(func(e *tk.Event) {
curr := slices.Index(directoryState.images, directoryState.currentFile)
switch e.Keysym {
case ".":
log.Printf("state: %+v", directoryState)
case "o":
newFileInDirectory(img)()
case "a":
if fileList == nil {
constructFileList()
} else {
// list closed by 'a'
destroyFileList(true)
}
case "Up", "Right":
if curr > 0 {
updateImage(filepath.Join(directoryState.currentDirectory, directoryState.images[curr-1]), img)
if lb != nil {
lb.SelectionClear("0", "end")
lb.SelectionSet(curr - 1)
lb.See(curr - 1)
}
}
case "Down", "Left":
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)
lb.See(curr + 1)
}
}
}
}))
// todo: resize image based on scroll events
// tk.Bind(tk.App, "<TouchpadScroll>", tk.Command(func(e *tk.Event) {
// log.Printf("%v, %v", int16(e.Delta>>16), int16(e.Delta&0xFFFF))
// }))
tk.Pack(img)
tk.App.Center()
tk.App.Wait()
}