From 75ea0d2d964b940d271e2bd3f0ce3416d76328cb Mon Sep 17 00:00:00 2001 From: g026r Date: Thu, 26 Sep 2024 02:32:08 -0400 Subject: [PATCH 1/4] Add the beginnings of an about screen & an overwrite setting # Conflicts: # pkg/ui/model.go # Conflicts: # pkg/ui/model.go --- pkg/io/io.go | 3 ++- pkg/ui/menus.go | 2 ++ pkg/ui/model.go | 27 +++++++++++++++++++-------- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/pkg/io/io.go b/pkg/io/io.go index 58306f2..65f27c5 100644 --- a/pkg/io/io.go +++ b/pkg/io/io.go @@ -35,6 +35,7 @@ type Config struct { AdvancedEditing bool `json:"advanced_editing"` ShowAdd bool `json:"show_add"` GenerateNew bool `json:"generate_new"` + Overwrite bool `json:"overwrite"` } type jsonEntry struct { @@ -42,7 +43,7 @@ type jsonEntry struct { Name string `json:"name"` Crc32 string `json:"crc"` Sig string `json:"signature"` - Magic string `json:"magic"` // TODO: Work out all possible mappings for this + Magic string `json:"magic"` // TODO: Work out all possible mappings for this? } func (j jsonEntry) Entry() models.Entry { diff --git a/pkg/ui/menus.go b/pkg/ui/menus.go index 3c2809d..2df9f50 100644 --- a/pkg/ui/menus.go +++ b/pkg/ui/menus.go @@ -39,6 +39,7 @@ const ( advEdit rmThumbs genNew + overwrite ) var ( @@ -90,6 +91,7 @@ var ( menuItem{"Generate new thumbnail when editing game", genNew}, //menuItem{"Show advanced library editing fields " + italic.Render("(Experimental)"), advEdit}, //menuItem{"Show 'Add to Library' " + italic.Render("(Experimental)"), showAdd}, + menuItem{"Overwrite original files on save' " + italic.Render("(Experimental)"), overwrite}, menuItem{"Back", back}} // esc consists of the items to be performed if esc is typed diff --git a/pkg/ui/model.go b/pkg/ui/model.go index 7b83b27..95ab416 100644 --- a/pkg/ui/model.go +++ b/pkg/ui/model.go @@ -280,13 +280,22 @@ func (m *Model) initSystem() tea.Msg { // save is the opposite of init: save our data to disk func (m *Model) save() tea.Msg { - wd, err := os.Getwd() - if err != nil { - return err - } - err = os.Mkdir(fmt.Sprintf("%s/pocket-toolkit", wd), os.ModePerm) - if err != nil && !os.IsExist(err) { - return errMsg{err, true} + // TODO: Overwrite in place + // TODO: If so, create a backup of the files first + var dir string + if m.Overwrite { + // TODO: Make backups + } else { + wd, err := os.Getwd() + if err != nil { + return err + } + dir = fmt.Sprintf("%s/pocket-toolkit", wd) + // Need to mkdir if we're overwriting + err = os.Mkdir(dir, os.ModePerm) + if err != nil && !os.IsExist(err) { + return errMsg{err, true} + } } ctr := 0.0 @@ -809,7 +818,7 @@ func (m *Model) processMenuItem(key menuKey) (*Model, tea.Cmd) { m.percent = 0.0 m.wait = "Generating thumbnails for all games in the Images folder. This may take a while." return m, tea.Batch(m.genFull, tickCmd()) - case showAdd, advEdit, rmThumbs, genNew: + case showAdd, advEdit, rmThumbs, genNew, overwrite: return m.configChange(key) } @@ -827,6 +836,8 @@ func (m *Model) configChange(key menuKey) (*Model, tea.Cmd) { m.AdvancedEditing = !m.AdvancedEditing case genNew: m.GenerateNew = !m.GenerateNew + case overwrite: + m.Overwrite = !m.Overwrite } return m, nil From da3c95785d5e79a7fc5b893ba095af9dd35a493a Mon Sep 17 00:00:00 2001 From: g026r Date: Fri, 27 Sep 2024 19:50:00 -0400 Subject: [PATCH 2/4] Working. --- README.md | 13 +++++---- cmd/gui/main.go | 1 + pkg/io/io.go | 3 ++ pkg/ui/menus.go | 68 +++++++++++++++++++++++--------------------- pkg/ui/model.go | 59 ++++++++++++++++++++------------------ pkg/ui/model_test.go | 12 ++++---- 6 files changed, 84 insertions(+), 72 deletions(-) diff --git a/README.md b/README.md index a184c57..2fb60e2 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,14 @@ experimental software that is doing something Analogue was not expecting users t ## How to Install -Download the appropriate version for your operating system from [the latest release](https://github.com/g026r/pocket-library-toolkit/releases/latest) & extract the archive. I recommend placing it in the root of your Pocket's SD card. +Download the appropriate version for your operating system +from [the latest release](https://github.com/g026r/pocket-library-toolkit/releases/latest) & extract the archive. I +recommend placing it in the root of your Pocket's SD card. Run the application & select the actions you desire. A basic [user guide](docs/userguide.md) is available. -Once complete, and then copy the files the tool generates over to the correct locations under your SD card's System directory _**making backups of any originals before replacing them**_. +Once complete, then copy the files the tool generates over to the correct locations under your SD card's System +directory _**making backups of any originals before replacing them**_. ## But Why? @@ -21,8 +24,8 @@ Because I can get remarkably anal about these things. First off: 95% of Pocket users won't need or even want this. -This software is for the users who are annoyed that their library shows `Famicom Mini 01 - Super Mario Bros.` but also -`Famicom Mini 22: Nazo no Murasame Jou`, that it's `The Lion King` but `NewZealand Story, The`. +This software is for the users who are annoyed that their library shows _Famicom Mini 01 - Super Mario Bros._ but also +_Famicom Mini 22: Nazo no Murasame Jou_, that it's _The Lion King_ but _NewZealand Story, The_. It's for those users who have one of the small number of carts that the Pocket misidentifies & who'd rather it appeared in their library under the correct name. @@ -33,7 +36,7 @@ manually editing the binary file themselves. ## Limitations The library info screen for a given cart is stored in the Pocket's internal memory. Even if your library -now shows "Sagaia" instead of "Mani 4 in 1 - Taito", clicking into it or loading the cart will still show you the +now shows "_Sagaia_" instead of "_Mani 4 in 1 - Taito_", clicking into it or loading the cart will still show you the original info. Additionally, if you have two different entries with the same cart signature, it's likely that only the playtime for the diff --git a/cmd/gui/main.go b/cmd/gui/main.go index 6c80f71..b079652 100644 --- a/cmd/gui/main.go +++ b/cmd/gui/main.go @@ -9,6 +9,7 @@ import ( ) func main() { + if _, err := tea.NewProgram(model2.NewModel(), tea.WithAltScreen()).Run(); err != nil { log.Fatal(err) } diff --git a/pkg/io/io.go b/pkg/io/io.go index 65f27c5..29ea803 100644 --- a/pkg/io/io.go +++ b/pkg/io/io.go @@ -35,6 +35,7 @@ type Config struct { AdvancedEditing bool `json:"advanced_editing"` ShowAdd bool `json:"show_add"` GenerateNew bool `json:"generate_new"` + SaveUnmodified bool `json:"save_unmodified"` Overwrite bool `json:"overwrite"` } @@ -250,6 +251,8 @@ func LoadConfig() (Config, error) { AdvancedEditing: false, ShowAdd: false, GenerateNew: true, + SaveUnmodified: false, + Overwrite: false, } //// FIXME: Use the program's dir rather than the cwd //dir, err := os.Getwd() diff --git a/pkg/ui/menus.go b/pkg/ui/menus.go index 2df9f50..2f86b90 100644 --- a/pkg/ui/menus.go +++ b/pkg/ui/menus.go @@ -25,21 +25,22 @@ const ( about save quit - add - edit - rm - fix + libAdd + libEdit + libRm + libFix back - missing - single - genlib - all - prune - showAdd - advEdit - rmThumbs - genNew - overwrite + tmMissing + tmSingle + tmGenlib + tmAll + tmPrune + cfgShowAdd + cfgAdvEdit + cfgRmThumbs + cfgGenNew + cfgUnmodified + cfgOverwrite ) var ( @@ -74,24 +75,25 @@ var ( menuItem{"Save & Quit", save}, menuItem{"Quit", quit}} libraryOptions = []list.Item{ - menuItem{"Add entry", add}, - menuItem{"Edit entry", edit}, - menuItem{"Remove entry", rm}, - menuItem{"Fix played times", fix}, + menuItem{"Add entry", libAdd}, + menuItem{"Edit entry", libEdit}, + menuItem{"Remove entry", libRm}, + menuItem{"Fix played times", libFix}, menuItem{"Back", back}} thumbOptions = []list.Item{ - menuItem{"Generate missing thumbnails", missing}, - menuItem{"Regenerate single game", single}, - menuItem{"Regenerate full library", genlib}, - menuItem{"Prune orphaned thumbnails", prune}, - menuItem{"Generate complete system thumbnails", all}, + menuItem{"Generate tmMissing thumbnails", tmMissing}, + menuItem{"Regenerate tmSingle game", tmSingle}, + menuItem{"Regenerate full library", tmGenlib}, + menuItem{"Prune orphaned thumbnails", tmPrune}, + menuItem{"Generate complete system thumbnails", tmAll}, menuItem{"Back", back}} configOptions = []list.Item{ - menuItem{"Remove thumbnail when removing game", rmThumbs}, - menuItem{"Generate new thumbnail when editing game", genNew}, - //menuItem{"Show advanced library editing fields " + italic.Render("(Experimental)"), advEdit}, - //menuItem{"Show 'Add to Library' " + italic.Render("(Experimental)"), showAdd}, - menuItem{"Overwrite original files on save' " + italic.Render("(Experimental)"), overwrite}, + menuItem{"Remove thumbnail when removing game", cfgRmThumbs}, + menuItem{"Generate new thumbnail when editing game", cfgGenNew}, + //menuItem{"Show advanced library editing fields " + italic.Render("(Experimental)"), cfgAdvEdit}, + //menuItem{"Show 'Add to Library' " + italic.Render("(Experimental)"), cfgShowAdd}, + menuItem{"Always save _thumbs.bin files, even if unmodified", cfgUnmodified}, + menuItem{"Overwrite original files on save' " + italic.Render("(Experimental)"), cfgOverwrite}, menuItem{"Back", back}} // esc consists of the items to be performed if esc is typed @@ -213,7 +215,7 @@ var ( } ) -// defaulAction is a default action for sub menus allowing numeric navigation. +// defaultAction is a default action for sub menus allowing numeric navigation. // It's not easily doable for game list menus as there may be too many items to handle key-presses without storing the previous press & waiting to process it. func defaultAction(scr screen, menu *list.Model, m *Model, msg tea.Msg) (*Model, tea.Cmd) { if k, ok := msg.(tea.KeyMsg); ok { @@ -305,13 +307,13 @@ func (d configDelegate) Render(w goio.Writer, m list.Model, index int, listItem str = fmt.Sprintf("%d. [%%s] %s", index+1, i) var b bool switch i.key { - case advEdit: + case cfgAdvEdit: b = d.AdvancedEditing - case showAdd: + case cfgShowAdd: b = d.ShowAdd - case rmThumbs: + case cfgRmThumbs: b = d.RemoveImages - case genNew: + case cfgGenNew: b = d.GenerateNew default: // If we don't know what this value is, return diff --git a/pkg/ui/model.go b/pkg/ui/model.go index 95ab416..8ecf022 100644 --- a/pkg/ui/model.go +++ b/pkg/ui/model.go @@ -131,14 +131,14 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, cmd case tea.WindowSizeMsg: m.progress.Width = msg.Width - 8 - m.mainMenu.SetHeight(msg.Height) - m.mainMenu.SetWidth(msg.Width) - m.subMenu.SetHeight(msg.Height) - m.subMenu.SetWidth(msg.Width) - m.configMenu.SetHeight(msg.Height) - m.configMenu.SetWidth(msg.Width) - m.gameList.SetHeight(msg.Height) - m.gameList.SetWidth(msg.Width) + m.mainMenu.SetHeight(msg.Height - 1) + m.mainMenu.SetWidth(msg.Width - 1) + m.subMenu.SetHeight(msg.Height - 1) + m.subMenu.SetWidth(msg.Width - 1) + m.configMenu.SetHeight(msg.Height - 1) + m.configMenu.SetWidth(msg.Width - 1) + m.gameList.SetHeight(msg.Height - 1) + m.gameList.SetWidth(msg.Width - 1) return m, nil case initDoneMsg: m.initialized = true @@ -281,7 +281,8 @@ func (m *Model) initSystem() tea.Msg { // save is the opposite of init: save our data to disk func (m *Model) save() tea.Msg { // TODO: Overwrite in place - // TODO: If so, create a backup of the files first + // TODO: If so, create a backup of the files first? + // TODO: Or temp files? (Temp files. Definitely temp files.) var dir string if m.Overwrite { // TODO: Make backups @@ -484,7 +485,7 @@ func (m *Model) regenLib() tea.Msg { return updateMsg{} } -// genSingle generates a single thumbnail entry & then either updates or inserts it into the list of thumbnails +// genSingle generates a tmSingle thumbnail entry & then either updates or inserts it into the list of thumbnails func (m *Model) genSingle(e models.Entry) tea.Cmd { return func() tea.Msg { m.percent = 0.0 @@ -770,7 +771,7 @@ func (m *Model) processMenuItem(key menuKey) (*Model, tea.Cmd) { return m, tea.Batch(m.save, tickCmd()) case back: return pop(m, nil) - case add: + case libAdd: m.focusedInput = 0 for i := range len(m.gameInput) { m.gameInput[i].Style(itemStyle) @@ -784,41 +785,41 @@ func (m *Model) processMenuItem(key menuKey) (*Model, tea.Cmd) { m.gameInput[m.focusedInput].Style(focusedStyle) m.Push(AddScreen) return m, m.gameInput[m.focusedInput].Focus() - case edit: + case libEdit: m.gameList = generateGameList(m.gameList, m.entries, "Main > Library > Edit Game", m.mainMenu.Width(), m.mainMenu.Height()) m.Push(EditList) - case rm: + case libRm: m.gameList = generateGameList(m.gameList, m.entries, "Main > Library > Remove Game", m.mainMenu.Width(), m.mainMenu.Height()) m.Push(RemoveList) - case fix: + case libFix: m.Push(Waiting) m.wait = "Fixing played times" m.percent = 0.0 return m, tea.Batch(m.playfix, tickCmd()) - case missing: + case tmMissing: m.Push(Waiting) m.percent = 0.0 - m.wait = "Generating missing thumbnails for library" + m.wait = "Generating tmMissing thumbnails for library" return m, tea.Batch(m.genMissing, tickCmd()) - case single: + case tmSingle: m.gameList = generateGameList(m.gameList, m.entries, "Main > Library > Generate Thumbnail", m.mainMenu.Width(), m.mainMenu.Height()) m.Push(GenerateList) - case genlib: + case tmGenlib: m.Push(Waiting) m.percent = 0.0 - m.wait = "Regenerating all thumbnails for library" + m.wait = "Regenerating tmAll thumbnails for library" return m, tea.Batch(m.regenLib, tickCmd()) - case prune: + case tmPrune: m.Push(Waiting) m.percent = 0.0 m.wait = "Removing orphaned thumbs.bin entries" return m, tea.Batch(m.prune, tickCmd()) - case all: + case tmAll: m.Push(Waiting) m.percent = 0.0 - m.wait = "Generating thumbnails for all games in the Images folder. This may take a while." + m.wait = "Generating thumbnails for tmAll games in the Images folder. This may take a while." return m, tea.Batch(m.genFull, tickCmd()) - case showAdd, advEdit, rmThumbs, genNew, overwrite: + case cfgShowAdd, cfgAdvEdit, cfgRmThumbs, cfgGenNew, cfgOverwrite, cfgUnmodified: return m.configChange(key) } @@ -828,16 +829,18 @@ func (m *Model) processMenuItem(key menuKey) (*Model, tea.Cmd) { // configMenu handles item selection on the settings menu func (m *Model) configChange(key menuKey) (*Model, tea.Cmd) { switch key { - case showAdd: + case cfgShowAdd: m.ShowAdd = !m.ShowAdd - case rmThumbs: + case cfgRmThumbs: m.RemoveImages = !m.RemoveImages - case advEdit: + case cfgAdvEdit: m.AdvancedEditing = !m.AdvancedEditing - case genNew: + case cfgGenNew: m.GenerateNew = !m.GenerateNew - case overwrite: + case cfgOverwrite: m.Overwrite = !m.Overwrite + case cfgUnmodified: + m.SaveUnmodified = !m.SaveUnmodified } return m, nil diff --git a/pkg/ui/model_test.go b/pkg/ui/model_test.go index a1eca9f..2670e05 100644 --- a/pkg/ui/model_test.go +++ b/pkg/ui/model_test.go @@ -101,17 +101,17 @@ func TestModel_configChange(t *testing.T) { sut := Model{Config: &config} // test all false -> true - m, _ := sut.configChange(showAdd) + m, _ := sut.configChange(cfgShowAdd) if !m.ShowAdd || m.AdvancedEditing || m.RemoveImages { t.Errorf("Expected ShowAdd to be true: %v", *m.Config) } *m.Config = io.Config{} - m, _ = sut.configChange(rmThumbs) + m, _ = sut.configChange(cfgRmThumbs) if !m.RemoveImages || m.AdvancedEditing || m.ShowAdd { t.Errorf("Expected RemoveImages to be true: %v", *m.Config) } *m.Config = io.Config{} - m, _ = sut.configChange(advEdit) + m, _ = sut.configChange(cfgAdvEdit) if !m.AdvancedEditing || m.ShowAdd || m.RemoveImages { t.Errorf("Expected AdvancedEditing to be true: %v", *m.Config) } @@ -123,17 +123,17 @@ func TestModel_configChange(t *testing.T) { } // test true -> false *sut.Config = allTrue - m, _ = sut.configChange(showAdd) + m, _ = sut.configChange(cfgShowAdd) if m.ShowAdd || !m.AdvancedEditing || !m.RemoveImages { t.Errorf("Expected ShowAdd to be false: %v", *m.Config) } *sut.Config = allTrue - m, _ = sut.configChange(rmThumbs) + m, _ = sut.configChange(cfgRmThumbs) if m.RemoveImages || !m.AdvancedEditing || !m.ShowAdd { t.Errorf("Expected RemoveImages to be false: %v", *m.Config) } *sut.Config = allTrue - m, _ = sut.configChange(advEdit) + m, _ = sut.configChange(cfgAdvEdit) if m.AdvancedEditing || !m.RemoveImages || !m.ShowAdd { t.Errorf("Expected AdvancedEditing to be false: %v", *m.Config) } From 2ac9b555771dda6f88c5363fd33e9f08edb0b536 Mon Sep 17 00:00:00 2001 From: g026r Date: Fri, 27 Sep 2024 21:49:31 -0400 Subject: [PATCH 3/4] Overwrite files on save option. --- cmd/gui/main.go | 1 - cmd/playfix/main.go | 14 +++-- pkg/io/io.go | 53 +++--------------- pkg/ui/menus.go | 16 +++--- pkg/ui/model.go | 128 +++++++++++++++++++++++++++++--------------- pkg/util/func.go | 34 ++++++++++++ 6 files changed, 146 insertions(+), 100 deletions(-) diff --git a/cmd/gui/main.go b/cmd/gui/main.go index b079652..6c80f71 100644 --- a/cmd/gui/main.go +++ b/cmd/gui/main.go @@ -9,7 +9,6 @@ import ( ) func main() { - if _, err := tea.NewProgram(model2.NewModel(), tea.WithAltScreen()).Run(); err != nil { log.Fatal(err) } diff --git a/cmd/playfix/main.go b/cmd/playfix/main.go index dc6a4a6..d9f6735 100644 --- a/cmd/playfix/main.go +++ b/cmd/playfix/main.go @@ -2,8 +2,10 @@ package main import ( "encoding/binary" + "fmt" "log" "os" + "path/filepath" "github.com/g026r/pocket-toolkit/pkg/io" ) @@ -11,11 +13,17 @@ import ( // Simple application to fix played times & nothing else. func main() { - entries, err := io.LoadEntries(os.DirFS("./")) + ex, err := os.Executable() if err != nil { log.Fatal(err) } - p, err := io.LoadPlaytimes(os.DirFS("./")) + root := filepath.Dir(ex) + + entries, err := io.LoadEntries(os.DirFS(root)) + if err != nil { + log.Fatal(err) + } + p, err := io.LoadPlaytimes(os.DirFS(root)) if err != nil { log.Fatal(err) } @@ -28,7 +36,7 @@ func main() { defer func() { _ = out.Close() if complete { // Overwrite the original with the temp file if successful; delete it if not. - err = os.Rename(out.Name(), "System/Played Games/playtimes.bin") + err = os.Rename(out.Name(), fmt.Sprintf("%s/System/Played Games/playtimes.bin", root)) } else { err = os.Remove(out.Name()) } diff --git a/pkg/io/io.go b/pkg/io/io.go index 29ea803..1b46459 100644 --- a/pkg/io/io.go +++ b/pkg/io/io.go @@ -254,7 +254,8 @@ func LoadConfig() (Config, error) { SaveUnmodified: false, Overwrite: false, } - //// FIXME: Use the program's dir rather than the cwd + // FIXME: When compiling, use the program's dir rather than the cwd + // FIXME: When testing, use the cwd & remember to comment out the filepath.Dir call //dir, err := os.Getwd() dir, err := os.Executable() if err != nil { @@ -306,23 +307,7 @@ func LoadInternal() (map[models.System][]models.Entry, error) { return library, nil } -func SaveLibrary(e []models.Entry, t map[uint32]models.PlayTime, tick chan any) error { - wd, err := os.Getwd() - if err != nil { - return err - } - l, err := os.Create(fmt.Sprintf("%s/pocket-toolkit/list.bin", wd)) - if err != nil { - return err - } - defer l.Close() - - p, err := os.Create(fmt.Sprintf("%s/pocket-toolkit/playtimes.bin", wd)) - if err != nil { - return err - } - defer p.Close() - +func SaveLibrary(l io.Writer, e []models.Entry, p io.Writer, t map[uint32]models.PlayTime, tick chan any) error { // Prep list.bin if err := binary.Write(l, binary.BigEndian, ListHeader); err != nil { return err @@ -381,32 +366,7 @@ func SaveLibrary(e []models.Entry, t map[uint32]models.PlayTime, tick chan any) return nil } -func SaveThumbs(t map[models.System]models.Thumbnails, tick chan any) error { - wd, err := os.Getwd() - if err != nil { - return err - } - - for sys, thumbs := range t { - if !thumbs.Modified { - continue // Not changed. For speed reasons, don't save. - } - - f, err := os.Create(fmt.Sprintf("%s/pocket-toolkit/%s_thumbs.bin", wd, strings.ToLower(sys.String()))) - if err != nil { - return err - } - - err = writeThumbsFile(f, thumbs.Images, tick) - _ = f.Close() // Close explicitly rather than defer as defer in a loop is not best practice - if err != nil { - return err - } - } - return nil -} - -func writeThumbsFile(t io.Writer, img []models.Image, tick chan any) error { +func SaveThumbsFile(t io.Writer, img []models.Image, tick chan any) error { if err := binary.Write(t, binary.LittleEndian, ThumbnailHeader); err != nil { return err } @@ -448,8 +408,9 @@ func SaveConfig(config Config) error { if err != nil { return err } - //// FIXME: Use the program's dir rather than the cwd - //dir, err := os.Getwd() + // FIXME: When compiling, use the program's dir rather than the cwd + // FIXME: When testing, use the cwd & remember to comment out the filepath.Dir call + // dir, err := os.Getwd() dir, err := os.Executable() if err != nil { return err diff --git a/pkg/ui/menus.go b/pkg/ui/menus.go index 2f86b90..22c41b6 100644 --- a/pkg/ui/menus.go +++ b/pkg/ui/menus.go @@ -90,10 +90,10 @@ var ( configOptions = []list.Item{ menuItem{"Remove thumbnail when removing game", cfgRmThumbs}, menuItem{"Generate new thumbnail when editing game", cfgGenNew}, + menuItem{"Overwrite original files on save", cfgOverwrite}, + menuItem{"Always save _thumbs.bin files, even if unmodified", cfgUnmodified}, //menuItem{"Show advanced library editing fields " + italic.Render("(Experimental)"), cfgAdvEdit}, //menuItem{"Show 'Add to Library' " + italic.Render("(Experimental)"), cfgShowAdd}, - menuItem{"Always save _thumbs.bin files, even if unmodified", cfgUnmodified}, - menuItem{"Overwrite original files on save' " + italic.Render("(Experimental)"), cfgOverwrite}, menuItem{"Back", back}} // esc consists of the items to be performed if esc is typed @@ -307,14 +307,18 @@ func (d configDelegate) Render(w goio.Writer, m list.Model, index int, listItem str = fmt.Sprintf("%d. [%%s] %s", index+1, i) var b bool switch i.key { - case cfgAdvEdit: - b = d.AdvancedEditing - case cfgShowAdd: - b = d.ShowAdd case cfgRmThumbs: b = d.RemoveImages case cfgGenNew: b = d.GenerateNew + case cfgUnmodified: + b = d.SaveUnmodified + case cfgOverwrite: + b = d.Overwrite + case cfgAdvEdit: + b = d.AdvancedEditing + case cfgShowAdd: + b = d.ShowAdd default: // If we don't know what this value is, return return diff --git a/pkg/ui/model.go b/pkg/ui/model.go index 8ecf022..7f21618 100644 --- a/pkg/ui/model.go +++ b/pkg/ui/model.go @@ -6,8 +6,8 @@ import ( "encoding/hex" "fmt" "io/fs" + "log" "os" - "path/filepath" "slices" "strings" "time" @@ -212,30 +212,9 @@ func aboutView() string { // initSystem loads all our data from disk func (m *Model) initSystem() tea.Msg { - var d string - var err error - - switch len(os.Args) { - case 1: - if d, err = os.Executable(); err != nil { - return errMsg{err, true} - } - d = filepath.Dir(d) - case 2: - d = os.Args[1] - default: - } - - d, err = filepath.Abs(d) - if err != nil { - return errMsg{err, true} - } - - fi, err := os.Stat(d) + d, err := util.GetRoot() if err != nil { return errMsg{err, true} - } else if !fi.IsDir() { - return errMsg{fmt.Errorf("%s is not a directory", d), true} } m.rootDir = os.DirFS(d) @@ -280,24 +259,83 @@ func (m *Model) initSystem() tea.Msg { // save is the opposite of init: save our data to disk func (m *Model) save() tea.Msg { - // TODO: Overwrite in place - // TODO: If so, create a backup of the files first? - // TODO: Or temp files? (Temp files. Definitely temp files.) - var dir string - if m.Overwrite { - // TODO: Make backups - } else { - wd, err := os.Getwd() - if err != nil { - return err - } - dir = fmt.Sprintf("%s/pocket-toolkit", wd) - // Need to mkdir if we're overwriting - err = os.Mkdir(dir, os.ModePerm) - if err != nil && !os.IsExist(err) { - return errMsg{err, true} + success := false + tmpList, err := os.CreateTemp("", "list.bin_*") + if err != nil { + return errMsg{err, true} + } + tmpPlaytimes, err := os.CreateTemp("", "playtimes.bin_*") + if err != nil { + return errMsg{err, true} + } + tmpThumbs := make(map[models.System]*os.File) + for k, v := range m.thumbnails { + if v.Modified || m.SaveUnmodified { + tmpThumbs[k], err = os.CreateTemp("", fmt.Sprintf("%s_thumbs.bin_*", strings.ToLower(k.String()))) + if err != nil { + return errMsg{err, true} + } } } + defer func() { + // An absolute mess of a function that: + // 1. Closes all the file handles + // 2. Creates the output directories if we're not overwriting + // 3. Moves the temporary files over to the correct spot if successful, or + // 4. Deletes them if we weren't + // TODO: Clean this up to make it all more manageable + + // Clean everything up + _ = tmpList.Close() + _ = tmpPlaytimes.Close() + for _, v := range tmpThumbs { + _ = v.Close() + } + if success { + var root string + var err error + if m.Overwrite { + root, err = util.GetRoot() + if err != nil { + log.Fatal(errorStyle.Render(err.Error())) + } + } else { + // If we're not overwriting in place, get the working dir & create all our directories if they don't exist + wd, err := os.Getwd() + if err != nil { + log.Fatal(errorStyle.Render(err.Error())) + } + root = fmt.Sprintf("%s/pocket-toolkit", wd) + _ = os.Mkdir(root, os.ModePerm) + _ = os.Mkdir(fmt.Sprintf("%s/System", root), os.ModePerm) + if len(tmpThumbs) > 0 { + _ = os.Mkdir(fmt.Sprintf("%s/System/Library", root), os.ModePerm) + _ = os.Mkdir(fmt.Sprintf("%s/System/Library/Images", root), os.ModePerm) + } + if err := os.Mkdir(fmt.Sprintf("%s/System/Played Games", root), os.ModePerm); err != nil && !os.IsExist(err) { + log.Fatal(errorStyle.Render(err.Error())) // Only going to check this final one for errors on the basis of "if it failed, the others did as well" + } + } + if err := os.Rename(tmpList.Name(), fmt.Sprintf("%s/System/Played Games/list.bin", root)); err != nil { + log.Fatal(errorStyle.Render(err.Error())) + } + if err := os.Rename(tmpPlaytimes.Name(), fmt.Sprintf("%s/System/Played Games/playtimes.bin", root)); err != nil { + log.Fatal(errorStyle.Render(err.Error())) + } + for k, v := range tmpThumbs { + if err := os.Rename(v.Name(), fmt.Sprintf("%s/System/Library/Images/%s_thumbs.bin", root, strings.ToLower(k.String()))); err != nil { + log.Fatal(errorStyle.Render(err.Error())) + } + } + } else { + // Remove all the files as we weren't successful + _ = os.Remove(tmpList.Name()) + _ = os.Remove(tmpPlaytimes.Name()) + for _, v := range tmpThumbs { + _ = os.Remove(v.Name()) + } + } + }() ctr := 0.0 tick := make(chan any) @@ -311,13 +349,15 @@ func (m *Model) save() tea.Msg { go func() { // Run these in a goroutine to avoid having to pass around the pointer to the progress value as that would require knowing the total as well defer close(tick) - if err := io.SaveLibrary(m.entries, m.playTimes, tick); err != nil { + if err := io.SaveLibrary(tmpList, m.entries, tmpPlaytimes, m.playTimes, tick); err != nil { tick <- err return } - if err := io.SaveThumbs(m.thumbnails, tick); err != nil { - tick <- err - return + for k, v := range tmpThumbs { + if err := io.SaveThumbsFile(v, m.thumbnails[k].Images, tick); err != nil { + tick <- err + return + } } if err := io.SaveConfig(*m.Config); err != nil { tick <- err @@ -336,6 +376,7 @@ func (m *Model) save() tea.Msg { } } + success = true return tea.QuitMsg{} } @@ -762,7 +803,6 @@ func (m *Model) processMenuItem(key menuKey) (*Model, tea.Cmd) { case about: m.Push(AboutScreen) m.anyKey = true - // TODO: Add a basic about screen case quit: return m, tea.Quit case save: diff --git a/pkg/util/func.go b/pkg/util/func.go index bf00c2f..640b95d 100644 --- a/pkg/util/func.go +++ b/pkg/util/func.go @@ -5,6 +5,8 @@ import ( "encoding/hex" "errors" "fmt" + "os" + "path/filepath" "strings" ) @@ -39,3 +41,35 @@ func HexStringTransform(s string) (uint32, error) { return binary.BigEndian.Uint32(h), nil } + +// GetRoot finds the path to the Pocket root dir. +// If an argument was passed, it uses that. +// If an argument wasn't passed, it uses the current directory. +func GetRoot() (string, error) { + var d string + var err error + switch len(os.Args) { + case 1: + if d, err = os.Executable(); err != nil { + return "", err + } + d = filepath.Dir(d) + case 2: + d = os.Args[1] + default: + } + + d, err = filepath.Abs(d) + if err != nil { + return "", err + } + + fi, err := os.Stat(d) + if err != nil { + return "", err + } else if !fi.IsDir() { + return "", fmt.Errorf("%s is not a directory", d) + } + + return d, nil +} From bdfba67183903ee18d867c8d1228907811cf76bd Mon Sep 17 00:00:00 2001 From: g026r Date: Fri, 27 Sep 2024 22:02:39 -0400 Subject: [PATCH 4/4] Futz with the progress bar on save. --- pkg/ui/model.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/ui/model.go b/pkg/ui/model.go index 7f21618..3bf2d38 100644 --- a/pkg/ui/model.go +++ b/pkg/ui/model.go @@ -339,13 +339,17 @@ func (m *Model) save() tea.Msg { ctr := 0.0 tick := make(chan any) + defer close(tick) total := float64(len(m.entries)) for _, v := range m.thumbnails { if v.Modified { total = total + float64(len(v.Images)) // Only increase the total if they've been modified since we don't write them out otherwise. } } - total = total + 1 // Add 1 for the config + // Add some extra to account for copying the files. + // This means the bar will never reach 100% since the tick channel is closed by then + // but better to pause for an extended period at 95% than at 100% + total = total + total/.95 go func() { // Run these in a goroutine to avoid having to pass around the pointer to the progress value as that would require knowing the total as well defer close(tick)