added archivefs package and support in cartridgeloader and ROM select window

archivefs allows opening of files inside a zip archive. support for
other archive file type will be added in the future when possible
This commit is contained in:
JetSetIlly 2024-04-16 11:12:46 +01:00
parent 8edee622cc
commit e3f4a743b6
14 changed files with 677 additions and 150 deletions

32
archivefs/archivefs.go Normal file
View file

@ -0,0 +1,32 @@
// This file is part of Gopher2600.
//
// Gopher2600 is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Gopher2600 is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Gopher2600. If not, see <https://www.gnu.org/licenses/>.
package archivefs
import "io"
// Open and return an io.ReadSeeker for the specified filename. Filename can be
// inside an archive supported by archivefs
//
// Returns the io.ReadSeeker, the size of the data behind the ReadSeeker and any
// errors.
func Open(filename string) (io.ReadSeeker, int, error) {
var afs Path
err := afs.Set(filename)
if err != nil {
return nil, 0, err
}
defer afs.Close()
return afs.Open()
}

126
archivefs/archivefs_test.go Normal file
View file

@ -0,0 +1,126 @@
// This file is part of Gopher2600.
//
// Gopher2600 is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Gopher2600 is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Gopher2600. If not, see <https://www.gnu.org/licenses/>.
package archivefs_test
import (
"fmt"
"io"
"path/filepath"
"testing"
"github.com/jetsetilly/gopher2600/archivefs"
"github.com/jetsetilly/gopher2600/test"
)
func TestArchivefsPath(t *testing.T) {
var afs archivefs.Path
var path string
var entries []archivefs.Node
var err error
// non-existant file
path = "foo"
err = afs.Set(path)
test.ExpectFailure(t, err)
test.ExpectEquality(t, afs.String(), "")
// a real directory
path = "testdir"
err = afs.Set(path)
test.ExpectSuccess(t, err)
test.ExpectEquality(t, afs.String(), path)
test.ExpectSuccess(t, afs.IsDir())
test.ExpectSuccess(t, !afs.InArchive())
// entries in a directory
entries, err = afs.List()
test.ExpectSuccess(t, err)
test.ExpectEquality(t, len(entries), 2)
test.ExpectEquality(t, fmt.Sprintf("%s", entries), "[testarchive.zip testfile]")
// non-existant file in directory
path = filepath.Join("testdir", "foo")
err = afs.Set(path)
test.ExpectFailure(t, err)
test.ExpectEquality(t, afs.String(), "")
// a real file in directory
path = filepath.Join("testdir", "testfile")
err = afs.Set(path)
test.ExpectSuccess(t, err)
test.ExpectEquality(t, afs.String(), path)
test.ExpectSuccess(t, !afs.IsDir())
test.ExpectSuccess(t, !afs.InArchive())
// calling List() when path is set to a file type (ie not a direcotry) the
// list returned should be of the containing directory
entries, err = afs.List()
test.ExpectSuccess(t, err)
test.ExpectEquality(t, len(entries), 2)
test.ExpectEquality(t, fmt.Sprintf("%s", entries), "[testarchive.zip testfile]")
// a real archive
path = filepath.Join("testdir", "testarchive.zip")
err = afs.Set(path)
test.ExpectSuccess(t, err)
test.ExpectEquality(t, afs.String(), path)
test.ExpectSuccess(t, afs.IsDir())
test.ExpectSuccess(t, afs.InArchive())
// entries in an archive
entries, err = afs.List()
test.ExpectSuccess(t, err)
test.ExpectEquality(t, len(entries), 3)
test.ExpectEquality(t, fmt.Sprintf("%s", entries), "[archivedir archivefile1 archivefile2]")
// file in a real archive
path = filepath.Join("testdir", "testarchive.zip", "archivefile1")
err = afs.Set(path)
test.ExpectSuccess(t, err)
test.ExpectEquality(t, afs.String(), path)
test.ExpectSuccess(t, !afs.IsDir())
test.ExpectSuccess(t, afs.InArchive())
// directory a real archive
path = filepath.Join("testdir", "testarchive.zip", "archivedir")
err = afs.Set(path)
test.ExpectSuccess(t, err)
test.ExpectEquality(t, afs.String(), path)
test.ExpectSuccess(t, afs.IsDir())
test.ExpectSuccess(t, afs.InArchive())
// entries in an archive
entries, err = afs.List()
test.ExpectSuccess(t, err)
test.ExpectEquality(t, len(entries), 2)
test.ExpectEquality(t, fmt.Sprintf("%s", entries), "[archivedir2 archivefile3]")
// file in a real archive
path = filepath.Join("testdir", "testarchive.zip", "archivedir", "archivefile3")
err = afs.Set(path)
test.ExpectSuccess(t, err)
test.ExpectEquality(t, afs.String(), path)
test.ExpectSuccess(t, !afs.IsDir())
test.ExpectSuccess(t, afs.InArchive())
}
func TestArchivefsOpen(t *testing.T) {
r, sz, err := archivefs.Open(filepath.Join("testdir", "testarchive.zip", "archivefile1"))
test.ExpectSuccess(t, err)
test.ExpectEquality(t, sz, 22)
d, err := io.ReadAll(r)
test.ExpectSuccess(t, err)
test.ExpectEquality(t, string(d), "archivefile1 contents\n")
}

20
archivefs/doc.go Normal file
View file

@ -0,0 +1,20 @@
// This file is part of Gopher2600.
//
// Gopher2600 is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Gopher2600 is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Gopher2600. If not, see <https://www.gnu.org/licenses/>.
// Package archivefs allows the traversal of a filetree and treating archive
// files transparently.
//
// Supported archive formats is currently limited to zip.
package archivefs

51
archivefs/extensions.go Normal file
View file

@ -0,0 +1,51 @@
// This file is part of Gopher2600.
//
// Gopher2600 is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Gopher2600 is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Gopher2600. If not, see <https://www.gnu.org/licenses/>.
package archivefs
import (
"fmt"
"path/filepath"
"strings"
)
// list of file extensions for the supported archive types
var ArchiveExtensions = [...]string{".ZIP"}
// RemoveArchiveExt removes the file extension of any supported/recognised
// archive type from within the string. Only the first instance of the extension
// is removed
func RemoveArchiveExt(s string) string {
t := strings.ToUpper(s)
for _, ext := range ArchiveExtensions {
i := strings.Index(t, ext)
if i >= 0 {
return fmt.Sprintf("%s%s", s[:i], s[i+len(ext):])
}
}
return s
}
// TrimArchiveExt removes the file extension of any supported/recognised archive
// type from the end of the string
func TrimArchiveExt(s string) string {
sext := strings.ToUpper(filepath.Ext(s))
for _, ext := range ArchiveExtensions {
if sext == ext {
return strings.TrimSuffix(s, filepath.Ext(s))
}
}
return s
}

289
archivefs/path.go Normal file
View file

@ -0,0 +1,289 @@
// This file is part of Gopher2600.
//
// Gopher2600 is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Gopher2600 is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Gopher2600. If not, see <https://www.gnu.org/licenses/>.
package archivefs
import (
"archive/zip"
"bytes"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"sort"
"strings"
)
// Node represents a single part of a full path
type Node struct {
Name string
// a directory has the the field of IsDir set to true
IsDir bool
// a recognised archive file has InArchive set to true. note that an archive
// file is also considered to be directory
IsArchive bool
}
func (e Node) String() string {
return e.Name
}
// Path represents a single destination in the file system
type Path struct {
current string
isDir bool
zf *zip.ReadCloser
// if the path is inside a zip file, we split the in-zip path into the path
// to a file and the file itself
inZipPath string
inZipFile string
}
// String returns the current path
func (afs Path) String() string {
return afs.current
}
// Base returns the last element of the current path
func (afs Path) Base() string {
return filepath.Base(afs.current)
}
// Dir returns all but the last element of path
func (afs Path) Dir() string {
if afs.isDir {
return afs.current
}
return filepath.Dir(afs.current)
}
// IsDir returns true if Path is currently set to a directory. For the purposes
// of archivefs, the root of an archive is treated as a directory
func (afs Path) IsDir() bool {
return afs.isDir
}
// InArchive returns true if path is currently inside an archive
func (afs Path) InArchive() bool {
return afs.zf != nil
}
// Open and return an io.ReadSeeker for the filename previously set by the Set()
// function.
//
// Returns the io.ReadSeeker, the size of the data behind the ReadSeeker and any
// errors.
func (afs Path) Open() (io.ReadSeeker, int, error) {
if afs.zf != nil {
f, err := afs.zf.Open(filepath.Join(afs.inZipPath, afs.inZipFile))
if err != nil {
return nil, 0, err
}
defer f.Close()
b, err := io.ReadAll(f)
if err != nil {
return nil, 0, err
}
return bytes.NewReader(b), len(b), nil
}
f, err := os.Open(afs.current)
if err != nil {
return nil, 0, err
}
info, err := f.Stat()
if err != nil {
return nil, 0, err
}
return f, int(info.Size()), nil
}
// Close any open zip files and reset path
func (afs *Path) Close() {
afs.current = ""
afs.isDir = false
afs.inZipPath = ""
afs.inZipFile = ""
if afs.zf != nil {
afs.zf.Close()
afs.zf = nil
}
}
// List returns the child entries for the current path location. If the current
// path is a file then the list will be the contents of the containing directory
// of that file
func (afs *Path) List() ([]Node, error) {
var ent []Node
if afs.zf != nil {
for _, f := range afs.zf.File {
// split file name into parts. the list is joined together again
// below to create the path to the file. this is better than
// filepath.Dir() because that will add path components that make it
// awkward to compare with afs.inZipPath
flst := strings.Split(filepath.Clean(f.Name), string(filepath.Separator))
fdir := filepath.Join(flst[:len(flst)-1]...)
// if path to the file is not the same as inZipPath then continue
// with the next file
if fdir != afs.inZipPath {
continue
}
fi := f.FileInfo()
if fi.IsDir() {
ent = append(ent, Node{
Name: fi.Name(),
IsDir: true,
})
} else {
ent = append(ent, Node{
Name: fi.Name(),
})
}
}
} else {
path := afs.current
if !afs.isDir {
path = filepath.Dir(path)
}
dir, err := os.ReadDir(path)
if err != nil {
return []Node{}, fmt.Errorf("archivefs: entries: %w", err)
}
for _, d := range dir {
// using os.Stat() to get file information otherwise links to
// directories do not have the IsDir() property
fi, err := os.Stat(filepath.Join(path, d.Name()))
if err != nil {
continue
}
if fi.IsDir() {
ent = append(ent, Node{
Name: d.Name(),
IsDir: true,
})
} else {
p := filepath.Join(path, d.Name())
_, err := zip.OpenReader(p)
if err == nil {
ent = append(ent, Node{
Name: d.Name(),
IsDir: true,
IsArchive: true,
})
} else {
ent = append(ent, Node{
Name: d.Name(),
})
}
}
}
}
// sort so that directories are at the start of the list
sort.Slice(ent, func(i int, j int) bool {
return ent[i].IsDir
})
// sort alphabetically (case insensitive)
sort.SliceStable(ent, func(i int, j int) bool {
return strings.ToLower(ent[i].Name) < strings.ToLower(ent[j].Name)
})
return ent, nil
}
func (afs *Path) Set(path string) error {
afs.Close()
// clean path and split into parts
path = filepath.Clean(path)
lst := strings.Split(path, string(filepath.Separator))
// strings.Split will remove a leading filepath.Separator. we need to add
// one back so that filepath.Join() works as expected
if lst[0] == "" {
lst[0] = string(filepath.Separator)
}
// reuse path string
path = ""
for _, l := range lst {
path = filepath.Join(path, l)
if afs.zf != nil {
p := filepath.Join(afs.inZipPath, l)
zf, err := afs.zf.Open(p)
if err != nil {
return fmt.Errorf("archivefs: set: %v", err)
}
zfi, err := zf.Stat()
if err != nil {
return fmt.Errorf("archivefs: set: %v", err)
}
afs.isDir = zfi.IsDir()
if afs.isDir {
afs.inZipPath = p
afs.inZipFile = ""
} else {
afs.inZipFile = l
}
} else {
fi, err := os.Stat(path)
if err != nil {
return fmt.Errorf("archivefs: set: %v", err)
}
afs.isDir = fi.IsDir()
if afs.isDir {
continue
}
afs.zf, err = zip.OpenReader(path)
if err == nil {
// the root of an archive file is considered to be a directory
afs.isDir = true
continue
}
if !errors.Is(err, zip.ErrFormat) {
return fmt.Errorf("archivefs: set: %v", err)
}
}
}
// make sure path is clean
afs.current = filepath.Clean(path)
return nil
}

Binary file not shown.

View file

@ -0,0 +1 @@
contents of testfile

View file

@ -24,12 +24,20 @@ import (
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"slices"
"strings"
"github.com/jetsetilly/gopher2600/archivefs"
)
// the maximum amount of data to load into the peep slice
const maxPeepLength = 1048576
func peepData(data []byte) []byte {
return data[:min(len(data), maxPeepLength)]
}
// Loader abstracts all the ways data can be loaded into the emulation.
type Loader struct {
io.ReadSeeker
@ -51,16 +59,24 @@ type Loader struct {
// empty string or "AUTO" indicates automatic fingerprinting
Mapping string
// hashes of data. will be empty if data is being streamed
// hashes of data
HashSHA1 string
HashMD5 string
// does the Data field consist of sound (PCM) data
IsSoundData bool
// cartridge data
data []byte
dataReader io.ReadSeeker
// data and size of
data io.ReadSeeker
size int
// peep is the data at the beginning of the cartridge data. it is used to
// help fingerprinting and for creating the SHA1 and MD5 hashes
//
// in reality, most cartridges are small enough to fit entirely inside the
// peep slice. currently it is only moviecart data and maybe supercharger
// sound files that will be larger than the maxPeepLength
peep []byte
// data was supplied through NewLoaderFromData()
embedded bool
@ -129,17 +145,14 @@ func NewLoaderFromFilename(filename string, mapping string) (Loader, error) {
// we want to do this now so we can initialise the stream
if ld.Mapping == "AUTO" {
ok, err := miniFingerprintMovieCart(filename)
if err != nil {
return Loader{}, fmt.Errorf("loader: %w", err)
}
if ok {
if err == nil && ok {
ld.Mapping = "MVC"
}
}
err = ld.open()
if err != nil {
return Loader{}, fmt.Errorf("loader: %w", err)
return Loader{}, err
}
// decide on the name for this cartridge
@ -173,13 +186,14 @@ func NewLoaderFromData(name string, data []byte, mapping string) (Loader, error)
}
ld := Loader{
Filename: name,
Mapping: mapping,
data: data,
dataReader: bytes.NewReader(data),
HashSHA1: fmt.Sprintf("%x", sha1.Sum(data)),
HashMD5: fmt.Sprintf("%x", md5.Sum(data)),
embedded: true,
Filename: name,
Mapping: mapping,
peep: peepData(data),
data: bytes.NewReader(data),
HashSHA1: fmt.Sprintf("%x", sha1.Sum(data)),
HashMD5: fmt.Sprintf("%x", md5.Sum(data)),
size: len(data),
embedded: true,
}
// decide on the name for this cartridge
@ -188,6 +202,14 @@ func NewLoaderFromData(name string, data []byte, mapping string) (Loader, error)
return ld, nil
}
func (ld *Loader) Reload() error {
err := ld.Close()
if err != nil {
return err
}
return ld.open()
}
// Reset prepares the loader for fresh reading. Useful to call after data has
// been Read() or if you need to make absolutely sure subsequent calls to Read()
// start from the beginning of the data stream
@ -200,54 +222,55 @@ func (ld *Loader) Reset() error {
//
// Should be called before disposing of a Loader instance.
func (ld *Loader) Close() error {
ld.data = nil
if closer, ok := ld.dataReader.(io.Closer); ok {
if closer, ok := ld.data.(io.Closer); ok {
err := closer.Close()
if err != nil {
return fmt.Errorf("loader: %w", err)
}
}
ld.data = nil
ld.size = 0
ld.peep = nil
return nil
}
// Implements the io.Reader interface.
func (ld Loader) Read(p []byte) (int, error) {
if ld.dataReader != nil {
return ld.dataReader.Read(p)
if ld.data != nil {
return ld.data.Read(p)
}
return 0, io.EOF
}
// Implements the io.Seeker interface.
func (ld Loader) Seek(offset int64, whence int) (int64, error) {
if ld.dataReader != nil {
return ld.dataReader.Seek(offset, whence)
if ld.data != nil {
return ld.data.Seek(offset, whence)
}
return 0, io.EOF
}
// Size returns the size of the cartridge data in bytes
func (ld Loader) Size() int {
return len(ld.data)
return ld.size
}
// Contains returns true if subslice appears anywhere in the data
func (ld Loader) Contains(subslice []byte) bool {
return bytes.Contains(ld.data, subslice)
return bytes.Contains(ld.peep, subslice)
}
// ContainsLimit returns true if subslice appears in the data at an offset between
// zero and limit
func (ld Loader) ContainsLimit(limit int, subslice []byte) bool {
limit = min(limit, ld.Size())
return bytes.Contains(ld.data[:limit], subslice)
return bytes.Contains(ld.peep[:limit], subslice)
}
// Count returns the number of non-overlapping instances of subslice in the data
func (ld Loader) Count(subslice []byte) int {
return bytes.Count(ld.data, subslice)
return bytes.Count(ld.peep, subslice)
}
// open the cartridge data. filenames with a valid schema will use that method
@ -271,41 +294,40 @@ func (ld *Loader) open() error {
}
defer resp.Body.Close()
ld.data, err = io.ReadAll(resp.Body)
data, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("loader: %w", err)
}
ld.data = bytes.NewReader(data)
ld.size = len(data)
ld.peep = peepData(data)
case "file":
fallthrough
default:
f, err := os.Open(ld.Filename)
r, sz, err := archivefs.Open(ld.Filename)
if err != nil {
return fmt.Errorf("loader: %w", err)
}
fs, err := f.Stat()
// peep at data
ld.peep, err = io.ReadAll(io.LimitReader(r, maxPeepLength))
if err != nil {
f.Close()
return fmt.Errorf("loader: %w", err)
}
if _, err := r.Seek(0, io.SeekStart); err != nil {
return fmt.Errorf("loader: %w", err)
}
if fs.Size() < 1048576 {
defer f.Close()
ld.data, err = io.ReadAll(f)
if err != nil {
return fmt.Errorf("loader: %w", err)
}
ld.dataReader = bytes.NewReader(ld.data)
} else {
ld.dataReader = f
}
ld.data = r
ld.size = sz
}
// generate hashes
ld.HashSHA1 = fmt.Sprintf("%x", sha1.Sum(ld.data))
ld.HashMD5 = fmt.Sprintf("%x", md5.Sum(ld.data))
ld.HashSHA1 = fmt.Sprintf("%x", sha1.Sum(ld.peep))
ld.HashMD5 = fmt.Sprintf("%x", md5.Sum(ld.peep))
return nil
}

View file

@ -32,9 +32,9 @@ func miniFingerprintMovieCart(filename string) (bool, error) {
if err != nil {
return false, err
}
defer f.Close()
b := make([]byte, 4)
f.Read(b)
f.Close()
if bytes.Compare(b, []byte{'M', 'V', 'C', 0x00}) == 0 {
return true, nil
}

View file

@ -963,7 +963,10 @@ func (dbg *Debugger) run() error {
return fmt.Errorf("debugger: %w", err)
}
} else if errors.Is(err, terminal.UserReload) {
dbg.reloadCartridge()
err = dbg.reloadCartridge()
if err != nil {
logger.Logf("debugger", err.Error())
}
} else {
return fmt.Errorf("debugger: %w", err)
}
@ -1450,25 +1453,16 @@ func (dbg *Debugger) Plugged(port plugging.PortID, peripheral plugging.Periphera
}
func (dbg *Debugger) reloadCartridge() error {
dbg.setState(govern.Initialising, govern.Normal)
spec := dbg.vcs.TV.GetFrameInfo().Spec.ID
err := dbg.insertCartridge("")
if err != nil {
return fmt.Errorf("debugger: %w", err)
}
// set spec to what it was before the cartridge insertion
err = dbg.vcs.TV.SetSpec(spec)
if err != nil {
return fmt.Errorf("debugger: %w", err)
if dbg.loader == nil {
return nil
}
// reset macro to beginning
if dbg.macro != nil {
dbg.macro.Reset()
}
return nil
return dbg.insertCartridge(dbg.loader.Filename)
}
// ReloadCartridge inserts the current cartridge and states the emulation over.

View file

@ -91,6 +91,8 @@ const (
TimelineComparison = '\uf02e'
TimelineComparisonLock = '\uf023'
Developer = '\uf0c3'
Paperclip = '\uf0c6'
Directory = '\uf07c'
)
// The first and last unicode points used in the application. We use this to

View file

@ -266,7 +266,7 @@ func newColors() *imguiColors {
PlayWindowBorder: imgui.Vec4{0.0, 0.0, 0.0, 1.0},
// ROM selector
ROMSelectDir: imgui.Vec4{1.0, 0.5, 0.5, 1.0},
ROMSelectDir: imgui.Vec4{0.5, 0.5, 1.0, 1.0},
ROMSelectFile: imgui.Vec4{1.0, 1.0, 1.0, 1.0},
// deferring CapturedScreenTitle & CapturedScreenBorder
@ -381,7 +381,7 @@ func newColors() *imguiColors {
// tia
TIApointer: imgui.Vec4{0.8, 0.8, 0.8, 1.0},
// deffering collision window CollisionBit
// deferring collision window CollisionBit
// deferring chip registers window RegisterBit
@ -438,6 +438,7 @@ func newColors() *imguiColors {
cols.SaveKeyBit = imgui.CurrentStyle().Color(imgui.StyleColorButton)
cols.TimelineWSYNC = cols.reflectionColors[reflection.WSYNC]
cols.TimelineCoProc = cols.reflectionColors[reflection.CoProcActive]
cols.CollisionBit = imgui.CurrentStyle().Color(imgui.StyleColorButton)
// colors that are used in context where an imgui.PackedColor is required
cols.windowBg = imgui.PackedColorFromVec4(cols.WindowBg)

View file

@ -27,7 +27,9 @@ import (
"golang.org/x/image/draw"
"github.com/inkyblackness/imgui-go/v4"
"github.com/jetsetilly/gopher2600/archivefs"
"github.com/jetsetilly/gopher2600/cartridgeloader"
"github.com/jetsetilly/gopher2600/gui/fonts"
"github.com/jetsetilly/gopher2600/hardware/peripherals"
"github.com/jetsetilly/gopher2600/hardware/television/specification"
"github.com/jetsetilly/gopher2600/logger"
@ -45,16 +47,15 @@ type winSelectROM struct {
img *SdlImgui
currPath string
entries []os.DirEntry
selectedFile string
selectedFileBase string
selectedFileProperties properties.Entry
path archivefs.Path
pathEntries []archivefs.Node
// selectedName is the name of the ROM in a normalised form
selectedName string
// properties of selected
selectedProperties properties.Entry
showAllFiles bool
showHidden bool
@ -142,30 +143,17 @@ func (win winSelectROM) id() string {
}
func (win *winSelectROM) setOpen(open bool) {
if open {
var err error
var p string
// open at the most recently selected ROM
f := win.img.dbg.Prefs.RecentROM.String()
if f == "" {
p, err = os.Getwd()
if err != nil {
logger.Logf("sdlimgui", err.Error())
}
} else {
p = filepath.Dir(f)
}
// set path and selected file
err = win.setPath(p)
if err != nil {
logger.Logf("sdlimgui", "error setting path (%s)", p)
}
win.setSelectedFile(f)
if !open {
win.path.Close()
return
}
// open at the most recently selected ROM
recent := win.img.dbg.Prefs.RecentROM.String()
err := win.setPath(recent)
if err != nil {
logger.Logf("sdlimgui", err.Error())
}
}
func (win *winSelectROM) playmodeSetOpen(open bool) {
@ -266,10 +254,10 @@ func (win *winSelectROM) render() {
func (win *winSelectROM) draw() {
// check for new property information
select {
case win.selectedFileProperties = <-win.propertyResult:
win.selectedName = win.selectedFileProperties.Name
case win.selectedProperties = <-win.propertyResult:
win.selectedName = win.selectedProperties.Name
if win.selectedName == "" {
win.selectedName = win.selectedFileBase
win.selectedName = win.path.Base()
win.selectedName = strings.TrimSuffix(win.selectedName, filepath.Ext(win.selectedName))
}
@ -291,7 +279,7 @@ func (win *winSelectROM) draw() {
}()
if imgui.Button("Parent") {
d := filepath.Dir(win.currPath)
d := filepath.Dir(win.path.Dir())
err := win.setPath(d)
if err != nil {
logger.Logf("sdlimgui", "error setting path (%s)", d)
@ -300,7 +288,7 @@ func (win *winSelectROM) draw() {
}
imgui.SameLine()
imgui.Text(win.currPath)
imgui.Text(archivefs.RemoveArchiveExt(win.path.Dir()))
if imgui.BeginTable("romSelector", 2) {
imgui.TableSetupColumnV("filelist", imgui.TableColumnFlagsWidthStretch, -1, 0)
@ -319,25 +307,27 @@ func (win *winSelectROM) draw() {
// list directories
imgui.PushStyleColor(imgui.StyleColorText, win.img.cols.ROMSelectDir)
for _, f := range win.entries {
for _, e := range win.pathEntries {
// ignore dot files
if !win.showHidden && f.Name()[0] == '.' {
if !win.showHidden && e.Name[0] == '.' {
continue
}
fi, err := os.Stat(filepath.Join(win.currPath, f.Name()))
if err != nil {
continue
}
if fi.Mode().IsDir() {
if e.IsDir {
s := strings.Builder{}
s.WriteString(f.Name())
s.WriteString(" [dir]")
if e.IsArchive {
s.WriteString(string(fonts.Paperclip))
s.WriteString(" ")
s.WriteString(archivefs.TrimArchiveExt(e.Name))
} else {
s.WriteString(string(fonts.Directory))
s.WriteString(" ")
s.WriteString(e.Name)
}
if imgui.Selectable(s.String()) {
d := filepath.Join(win.currPath, f.Name())
err = win.setPath(d)
d := filepath.Join(win.path.Dir(), e.Name)
err := win.setPath(d)
if err != nil {
logger.Logf("sdlimgui", "error setting path (%s)", d)
}
@ -349,19 +339,14 @@ func (win *winSelectROM) draw() {
// list files
imgui.PushStyleColor(imgui.StyleColorText, win.img.cols.ROMSelectFile)
for _, f := range win.entries {
for _, e := range win.pathEntries {
// ignore dot files
if !win.showHidden && f.Name()[0] == '.' {
continue
}
fi, err := os.Stat(filepath.Join(win.currPath, f.Name()))
if err != nil {
if !win.showHidden && e.Name[0] == '.' {
continue
}
// ignore invalid file extensions unless showAllFiles flags is set
ext := strings.ToUpper(filepath.Ext(fi.Name()))
ext := strings.ToUpper(filepath.Ext(e.Name))
if !win.showAllFiles {
hasExt := false
for _, e := range cartridgeloader.FileExtensions {
@ -370,20 +355,28 @@ func (win *winSelectROM) draw() {
break
}
}
if !hasExt {
for _, e := range archivefs.ArchiveExtensions {
if e == ext {
hasExt = true
break
}
}
}
if !hasExt {
continue // to next file
}
}
if fi.Mode().IsRegular() {
selected := f.Name() == win.selectedFileBase
if !e.IsDir {
selected := e.Name == win.path.Base()
if selected && win.centreOnFile {
imgui.SetScrollHereY(0.0)
}
if imgui.SelectableV(f.Name(), selected, 0, imgui.Vec2{0, 0}) {
win.setSelectedFile(filepath.Join(win.currPath, f.Name()))
if imgui.SelectableV(e.Name, selected, 0, imgui.Vec2{0, 0}) {
win.setPath(filepath.Join(win.path.Dir(), e.Name))
}
if imgui.IsItemHovered() && imgui.IsMouseDoubleClicked(0) {
win.insertCartridge()
@ -438,8 +431,8 @@ func (win *winSelectROM) draw() {
imgui.TableNextColumn()
imgui.Text("Name")
imgui.TableNextColumn()
if win.selectedFileProperties.IsValid() {
imgui.Text(win.selectedFileProperties.Name)
if win.selectedProperties.IsValid() {
imgui.Text(win.selectedProperties.Name)
} else {
imgui.Text(win.selectedName)
}
@ -507,38 +500,38 @@ func (win *winSelectROM) draw() {
imgui.PopStyleVar()
imgui.PopItemFlag()
if win.selectedFileProperties.Manufacturer != "" {
if win.selectedProperties.Manufacturer != "" {
imgui.TableNextRow()
imgui.TableNextColumn()
imgui.AlignTextToFramePadding()
imgui.Text("Manufacturer")
imgui.TableNextColumn()
imgui.Text(win.selectedFileProperties.Manufacturer)
imgui.Text(win.selectedProperties.Manufacturer)
}
if win.selectedFileProperties.Rarity != "" {
if win.selectedProperties.Rarity != "" {
imgui.TableNextRow()
imgui.TableNextColumn()
imgui.AlignTextToFramePadding()
imgui.Text("Rarity")
imgui.TableNextColumn()
imgui.Text(win.selectedFileProperties.Rarity)
imgui.Text(win.selectedProperties.Rarity)
}
if win.selectedFileProperties.Model != "" {
if win.selectedProperties.Model != "" {
imgui.TableNextRow()
imgui.TableNextColumn()
imgui.AlignTextToFramePadding()
imgui.Text("Model")
imgui.TableNextColumn()
imgui.Text(win.selectedFileProperties.Model)
imgui.Text(win.selectedProperties.Model)
}
if win.selectedFileProperties.Note != "" {
if win.selectedProperties.Note != "" {
imgui.TableNextRow()
imgui.TableNextColumn()
imgui.AlignTextToFramePadding()
imgui.Text("Note")
imgui.TableNextColumn()
imgui.Text(win.selectedFileProperties.Note)
imgui.Text(win.selectedProperties.Note)
}
imgui.EndTable()
@ -590,11 +583,11 @@ func (win *winSelectROM) draw() {
win.playmodeSetOpen(false)
}
if win.selectedFile != "" {
if win.selectedName != "" {
var s string
// load or reload button
if win.selectedFile == win.img.cache.VCS.Mem.Cart.Filename {
if win.path.String() == win.img.cache.VCS.Mem.Cart.Filename {
s = fmt.Sprintf("Reload %s", win.selectedName)
} else {
s = fmt.Sprintf("Load %s", win.selectedName)
@ -621,7 +614,7 @@ func (win *winSelectROM) insertCartridge() {
return
}
win.img.dbg.InsertCartridge(win.selectedFile)
win.img.dbg.InsertCartridge(win.path.String())
// close rom selected in both the debugger and playmode
win.debuggerSetOpen(false)
@ -630,33 +623,28 @@ func (win *winSelectROM) insertCartridge() {
func (win *winSelectROM) setPath(path string) error {
var err error
win.currPath = filepath.Clean(path)
win.entries, err = os.ReadDir(win.currPath)
win.path.Set(path)
win.pathEntries, err = win.path.List()
if err != nil {
return err
}
win.setSelectedFile("")
if win.path.IsDir() {
win.setSelectedFile("")
} else {
win.setSelectedFile(win.path.String())
}
return nil
}
func (win *winSelectROM) setSelectedFile(filename string) {
// do nothing if this file has already been selected
if win.selectedFile == filename {
return
}
// update selected file. return immediately if the filename is empty
win.selectedFile = filename
if filename == "" {
win.selectedFileBase = ""
win.selectedName = ""
return
}
// base filename for presentation purposes
win.selectedFileBase = filepath.Base(filename)
// selected name will be
win.selectedName = ""
// return immediately if the filename is empty
if filename == "" {
return
}
// create cartridge loader and start thumbnail emulation
loader, err := cartridgeloader.NewLoaderFromFilename(filename, "AUTO")
if err != nil {
@ -679,7 +667,7 @@ func (win *winSelectROM) findBoxart() error {
win.boxartUse = false
// fuzzy find a candidate image
n, _, _ := strings.Cut(win.selectedFileProperties.Name, "(")
n, _, _ := strings.Cut(win.selectedProperties.Name, "(")
n = strings.TrimSpace(n)
m := fuzzy.Find(n, win.boxart)
if len(m) == 0 {

View file

@ -345,6 +345,7 @@ func (cart *Moviecart) Plumb(env *environment.Environment) {
// Reset implements the mapper.CartMapper interface.
func (cart *Moviecart) Reset() {
cart.state.initialise()
}
// Access implements the mapper.CartMapper interface.