├── .github └── workflows │ └── xcodebuild.yml ├── .gitignore ├── .travis.yml ├── Ample.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcuserdata │ └── kelvin.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── Ample ├── Ample.entitlements ├── Ample.h ├── Ample.m ├── AmpleLite.m ├── AppDelegate.h ├── AppDelegate.m ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── icon-1024.png │ │ ├── icon-128.png │ │ ├── icon-16.png │ │ ├── icon-256.png │ │ ├── icon-32.png │ │ ├── icon-512.png │ │ └── icon-64.png │ └── Contents.json ├── AutocompleteControl.h ├── AutocompleteControl.m ├── Base.lproj │ ├── Autocomplete.xib │ ├── BookmarkWindow.xib │ ├── CheatSheet.xib │ ├── Credits.rtf │ ├── DiskImages.xib │ ├── DownloadWindow.xib │ ├── LaunchWindow.xib │ ├── LogWindow.xib │ ├── MachineView.xib │ ├── MainMenu.xib │ ├── MediaView.xib │ ├── Preferences.xib │ └── SlotView.xib ├── Bookmark.h ├── BookmarkManager.h ├── BookmarkManager.m ├── BookmarkWindowController.h ├── BookmarkWindowController.m ├── CheatSheetWindowController.h ├── CheatSheetWindowController.m ├── Core Data │ ├── Ample.xcdatamodeld │ │ └── Ample.xcdatamodel │ │ │ └── contents │ ├── Bookmark+CoreDataClass.h │ ├── Bookmark+CoreDataClass.m │ ├── Bookmark+CoreDataProperties.h │ ├── Bookmark+CoreDataProperties.m │ ├── DiskImage+CoreDataClass.h │ ├── DiskImage+CoreDataClass.m │ ├── DiskImage+CoreDataProperties.h │ └── DiskImage+CoreDataProperties.m ├── Defaults.plist ├── DiskImage.h ├── DiskImagesWindowController.h ├── DiskImagesWindowController.m ├── DownloadWindowController.h ├── DownloadWindowController.m ├── EjectButton.h ├── EjectButton.m ├── FlippedView.h ├── FlippedView.m ├── Info.plist ├── LaunchWindowController.h ├── LaunchWindowController.m ├── LogWindowController.h ├── LogWindowController.m ├── MachineViewController.h ├── MachineViewController.m ├── Media.h ├── Media.m ├── MediaViewController.h ├── MediaViewController.m ├── Menu.h ├── Menu.m ├── MidiManager.h ├── MidiManager.m ├── NewMachineViewController.h ├── NewMachineViewController.m ├── PreferencesWindowController.h ├── PreferencesWindowController.m ├── Resources │ ├── CheatSheet.html │ ├── a1000.plist │ ├── a1000n.plist │ ├── a2000.plist │ ├── a2000n.plist │ ├── a500.plist │ ├── a500n.plist │ ├── ace100.plist │ ├── ace1000.plist │ ├── ace2200.plist │ ├── ace500.plist │ ├── agat7.plist │ ├── agat9.plist │ ├── albert.plist │ ├── am100.plist │ ├── am64.plist │ ├── apple1.plist │ ├── apple2.plist │ ├── apple2c.plist │ ├── apple2c0.plist │ ├── apple2c3.plist │ ├── apple2c4.plist │ ├── apple2cp.plist │ ├── apple2e.plist │ ├── apple2ede.plist │ ├── apple2ee.plist │ ├── apple2eede.plist │ ├── apple2eefr.plist │ ├── apple2ees.plist │ ├── apple2eese.plist │ ├── apple2eeuk.plist │ ├── apple2efr.plist │ ├── apple2ep.plist │ ├── apple2epde.plist │ ├── apple2epfr.plist │ ├── apple2epse.plist │ ├── apple2epuk.plist │ ├── apple2ese.plist │ ├── apple2euk.plist │ ├── apple2gs.plist │ ├── apple2gsmt.plist │ ├── apple2gsr0.plist │ ├── apple2gsr1.plist │ ├── apple2jp.plist │ ├── apple2p.plist │ ├── apple3.plist │ ├── basis108.plist │ ├── bbca.plist │ ├── bbcb.plist │ ├── bbcb_de.plist │ ├── bbcb_no.plist │ ├── bbcb_us.plist │ ├── bbcbp.plist │ ├── bbcbp128.plist │ ├── bbcm.plist │ ├── bbcmc.plist │ ├── bbcmt.plist │ ├── c128.plist │ ├── c64.plist │ ├── c64c.plist │ ├── cec2000.plist │ ├── cece.plist │ ├── cecg.plist │ ├── ceci.plist │ ├── cecm.plist │ ├── coco.plist │ ├── coco2b.plist │ ├── coco2bh.plist │ ├── coco3.plist │ ├── coco3h.plist │ ├── coco3p.plist │ ├── cocoh.plist │ ├── craft2p.plist │ ├── cz101.plist │ ├── d64plus.plist │ ├── dodo.plist │ ├── dragon200.plist │ ├── dragon200e.plist │ ├── dragon32.plist │ ├── dragon64.plist │ ├── ds2100.plist │ ├── ds3100.plist │ ├── ds5k133.plist │ ├── electron.plist │ ├── elppa.plist │ ├── hkc8800a.plist │ ├── hp9k310.plist │ ├── hp9k320.plist │ ├── hp9k330.plist │ ├── hp9k332.plist │ ├── hp9k340.plist │ ├── hp9k360.plist │ ├── hp9k370.plist │ ├── hp9k380.plist │ ├── hp9k382.plist │ ├── indigo.plist │ ├── indigo2_4415.plist │ ├── indigo_r4000.plist │ ├── indigo_r4400.plist │ ├── indy_4610.plist │ ├── indy_4613.plist │ ├── indy_5015.plist │ ├── ip2000.plist │ ├── ip2400.plist │ ├── ip2500.plist │ ├── ip2700.plist │ ├── ip2800.plist │ ├── ip6000.plist │ ├── ip6400.plist │ ├── ip6700.plist │ ├── ip6800.plist │ ├── ivelultr.plist │ ├── las128e2.plist │ ├── las128ex.plist │ ├── las3000.plist │ ├── laser128.plist │ ├── laser128o.plist │ ├── laser2c.plist │ ├── mac128k.plist │ ├── mac2fdhd.plist │ ├── mac512k.plist │ ├── mac512ke.plist │ ├── maccclas.plist │ ├── macclas2.plist │ ├── macclasc.plist │ ├── macct610.plist │ ├── macct650.plist │ ├── macii.plist │ ├── maciici.plist │ ├── maciicx.plist │ ├── maciifx.plist │ ├── maciihmu.plist │ ├── maciisi.plist │ ├── maciivi.plist │ ├── maciivx.plist │ ├── maciix.plist │ ├── maclc.plist │ ├── maclc2.plist │ ├── maclc3.plist │ ├── maclc3p.plist │ ├── maclc475.plist │ ├── maclc520.plist │ ├── maclc550.plist │ ├── maclc575.plist │ ├── maclc580.plist │ ├── macpb100.plist │ ├── macpb140.plist │ ├── macpb145.plist │ ├── macpb145b.plist │ ├── macpb160.plist │ ├── macpb165.plist │ ├── macpb165c.plist │ ├── macpb170.plist │ ├── macpb180.plist │ ├── macpb180c.plist │ ├── macpd210.plist │ ├── macpd230.plist │ ├── macpd250.plist │ ├── macpd270c.plist │ ├── macpd280.plist │ ├── macpd280c.plist │ ├── macplus.plist │ ├── macprtb.plist │ ├── macqd605.plist │ ├── macqd610.plist │ ├── macqd630.plist │ ├── macqd650.plist │ ├── macqd700.plist │ ├── macqd800.plist │ ├── macqd900.plist │ ├── macqd950.plist │ ├── macse.plist │ ├── macse30.plist │ ├── macsefd.plist │ ├── mactv.plist │ ├── maxxi.plist │ ├── mc10.plist │ ├── megast.plist │ ├── microeng.plist │ ├── models.plist │ ├── models~extra.plist │ ├── mprof3.plist │ ├── nws1580.plist │ ├── nws3260.plist │ ├── nws3410.plist │ ├── nws5000x.plist │ ├── oric1.plist │ ├── orica.plist │ ├── pdp11qb.plist │ ├── pdp11ub.plist │ ├── pdp11ub2.plist │ ├── pi4d20.plist │ ├── pi4d25.plist │ ├── pi4d30.plist │ ├── pi4d35.plist │ ├── prav82.plist │ ├── prav8c.plist │ ├── prav8d.plist │ ├── prav8m.plist │ ├── rc2030.plist │ ├── rc3230.plist │ ├── roms.plist │ ├── roms~extra.plist │ ├── rs2030.plist │ ├── rs3230.plist │ ├── rtpc010.plist │ ├── rtpc015.plist │ ├── rtpc020.plist │ ├── rtpc025.plist │ ├── rtpca25.plist │ ├── space84.plist │ ├── spectred.plist │ ├── st.plist │ ├── sun1.plist │ ├── sun2_120.plist │ ├── sun2_50.plist │ ├── sun3_110.plist │ ├── sun3_150.plist │ ├── sun3_260.plist │ ├── sun3_50.plist │ ├── sun3_60.plist │ ├── sun3_80.plist │ ├── sun3_e.plist │ ├── sun4_20.plist │ ├── sun4_25.plist │ ├── sun4_40.plist │ ├── sun4_50.plist │ ├── sun4_65.plist │ ├── tanodr64.plist │ ├── telstrat.plist │ ├── tk3000.plist │ ├── trs80.plist │ ├── trs80l2.plist │ ├── uniap2en.plist │ ├── uniap2pt.plist │ ├── uniap2ti.plist │ ├── vt100.plist │ ├── vt101.plist │ ├── vt102.plist │ ├── vt240.plist │ ├── vt52.plist │ └── zijini.plist ├── Slot.h ├── Slot.m ├── SlotViewController.h ├── SlotViewController.m ├── SoftwareList.h ├── SoftwareList.m ├── TableCellView.h ├── TableCellView.m ├── Transformers.h ├── Transformers.m ├── TransparentScroller.h ├── TransparentScroller.m ├── images │ ├── caution.png │ ├── caution@2x.png │ ├── caution@3x.png │ ├── drag-handle-4x10.png │ ├── drag-handle-4x10@2x.png │ ├── drag-handle-4x10@3x.png │ ├── eject-16x16.png │ ├── eject-16x16@2x.png │ ├── eject-16x16@3x.png │ ├── eject-hover-16x16.png │ ├── eject-hover-16x16@2x.png │ └── eject-hover-16x16@3x.png └── main.m ├── README.md ├── embedded ├── Host.FST.po ├── Host.MLI.po ├── README.md ├── download-sdl.sh ├── download-sparkle.sh ├── install_name_tool.pl └── mame64.entitlements ├── pty_shell ├── pty_shell.c └── pty_shell.entitlements ├── python ├── listmedia.sh ├── listslots.sh ├── listxml.sh ├── machines.py ├── mame.py ├── mkdevices.py ├── mkmachines.py ├── mkmodels.py ├── mkroms.py ├── plist.py └── rom.py ├── screenshots ├── 2020-08-30.png ├── 2020-09-06.png ├── 2020-09-14.png ├── 2021-07-01.png └── 2021-07-01@2x.png └── vmnet_helper ├── vmnet_helper.c └── vmnet_helper.entitlements /.github/workflows/xcodebuild.yml: -------------------------------------------------------------------------------- 1 | name: xcodebuild 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: macos-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v4 12 | 13 | - name: build 14 | run: xcodebuild -project "Ample.xcodeproj" -target "Ample Lite" | xcpretty 15 | 16 | - uses: actions/upload-artifact@v4 17 | with: 18 | name: Ample Lite 19 | path: build/Release/ 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | embedded/mame64 3 | embedded/SDL2.framework 4 | embedded/Sparkle.framework 5 | embedded/Sparkle-* 6 | embedded/SDL2-* 7 | build 8 | embedded/mame-data.tgz 9 | __pycache__ 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | 3 | osx_image: 4 | - xcode12 5 | - xcode11.3 6 | - xcode10.1 7 | xcode_project: Ample.xcodeproj 8 | xcode_scheme: Ample Lite 9 | 10 | script: 11 | - set -o pipefail 12 | - xcodebuild -project "${TRAVIS_XCODE_PROJECT}" -target "Ample Lite" | xcpretty 13 | -------------------------------------------------------------------------------- /Ample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Ample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Ample.xcodeproj/xcuserdata/kelvin.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Ample Lite.xcscheme_^#shared#^_ 8 | 9 | isShown 10 | 11 | orderHint 12 | 1 13 | 14 | Ample copy.xcscheme_^#shared#^_ 15 | 16 | orderHint 17 | 1 18 | 19 | Ample.xcscheme_^#shared#^_ 20 | 21 | orderHint 22 | 0 23 | 24 | MA2ME.xcscheme_^#shared#^_ 25 | 26 | orderHint 27 | 0 28 | 29 | pty_shell.xcscheme_^#shared#^_ 30 | 31 | orderHint 32 | 3 33 | 34 | vmnet_helper.xcscheme_^#shared#^_ 35 | 36 | isShown 37 | 38 | orderHint 39 | 2 40 | 41 | 42 | SuppressBuildableAutocreation 43 | 44 | B6841BCF251EC913006A5C39 45 | 46 | primary 47 | 48 | 49 | B6BA257A24E99BE9005FB8FF 50 | 51 | primary 52 | 53 | 54 | B6E4B5AE24FDE2670094A35C 55 | 56 | primary 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /Ample/Ample.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.cs.disable-library-validation 8 | 9 | com.apple.security.files.user-selected.read-only 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Ample/Ample.h: -------------------------------------------------------------------------------- 1 | // 2 | // Ample.h 3 | // Ample 4 | // 5 | // Created by Kelvin Sherlock on 9/1/2020. 6 | // Copyright © 2020 Kelvin Sherlock. All rights reserved. 7 | // 8 | 9 | #ifndef Ample_h 10 | #define Ample_h 11 | 12 | #import 13 | 14 | /* ~/Library/ApplicationSupport/Ample/ */ 15 | NSURL *SupportDirectory(void); 16 | NSString *SupportDirectoryPath(void); 17 | 18 | /* mame executable URL */ 19 | NSURL *MameURL(void); 20 | NSString *MamePath(void); 21 | 22 | /* mame working directory */ 23 | NSURL *MameWorkingDirectory(void); 24 | NSString *MameWorkingDirectoryPath(void); 25 | 26 | NSString *InternString(NSString *key); 27 | 28 | NSDictionary *MameMachine(NSString *machine); 29 | 30 | /* NSUserDefaults keys */ 31 | extern NSString *kUseCustomMame; 32 | extern NSString *kMamePath; 33 | extern NSString *kMameWorkingDirectory; 34 | extern NSString *kAutoCloseLogWindow; 35 | extern NSString *kMameComponentsDate; 36 | extern NSString *kUseLogWindow; 37 | 38 | extern NSString *kDownloadURL; 39 | extern NSString *kDownloadExtension; 40 | 41 | extern NSString *kDefaultDownloadURL; 42 | extern NSString *kDefaultDownloadExtension; 43 | 44 | extern NSString *kNotificationDiskImageAdded; 45 | extern NSString *kNotificationDiskImageMagicRoute; 46 | extern NSString *kNotificationBookmarkMagicRoute; 47 | 48 | @protocol Bookmark 49 | -(BOOL)loadBookmark: (NSDictionary *)bookmark; 50 | -(BOOL)saveBookmark: (NSMutableDictionary *)bookmark; 51 | 52 | -(void)willLoadBookmark: (NSDictionary *)bookmark; 53 | -(void)didLoadBookmark: (NSDictionary *)bookmark; 54 | @end 55 | 56 | #endif /* Ample_h */ 57 | -------------------------------------------------------------------------------- /Ample/Ample.m: -------------------------------------------------------------------------------- 1 | // 2 | // Ample.m 3 | // Ample 4 | // 5 | // Created by Kelvin Sherlock on 9/1/2020. 6 | // Copyright © 2020 Kelvin Sherlock. All rights reserved. 7 | // 8 | 9 | #include "Ample.h" 10 | 11 | NSURL *SupportDirectory(void) { 12 | static NSURL *cached = nil; 13 | 14 | if (!cached) { 15 | NSFileManager *fm = [NSFileManager defaultManager]; 16 | NSError *error = nil; 17 | 18 | NSURL *url = [fm URLForDirectory: NSApplicationSupportDirectory inDomain: NSUserDomainMask appropriateForURL: nil create: YES error: &error]; 19 | cached = [url URLByAppendingPathComponent: @"Ample"]; 20 | 21 | [fm createDirectoryAtURL: cached withIntermediateDirectories: YES attributes: nil error: &error]; 22 | } 23 | return cached; 24 | 25 | } 26 | 27 | NSString *SupportDirectoryPath(void) { 28 | static NSString *cached = nil; 29 | 30 | if (!cached) { 31 | NSURL *url = SupportDirectory(); 32 | cached = [NSString stringWithCString: [url fileSystemRepresentation] encoding: NSUTF8StringEncoding]; 33 | } 34 | return cached; 35 | } 36 | 37 | 38 | NSURL *MameURL(void) { 39 | 40 | NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; 41 | NSBundle *bundle = [NSBundle mainBundle]; 42 | 43 | if ([defaults boolForKey: kUseCustomMame]) { 44 | NSString *path = [defaults stringForKey: kMamePath]; 45 | if ([path length]) return [NSURL fileURLWithPath: path]; 46 | } 47 | 48 | return [bundle URLForAuxiliaryExecutable: @"mame64"]; 49 | } 50 | 51 | NSString *MamePath(void) { 52 | 53 | NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; 54 | NSBundle *bundle = [NSBundle mainBundle]; 55 | 56 | NSString *path; 57 | 58 | if ([defaults boolForKey: kUseCustomMame]) { 59 | path = [defaults stringForKey: kMamePath]; 60 | if ([path length]) return path; 61 | } 62 | path = [bundle pathForAuxiliaryExecutable: @"mame64"]; 63 | if ([path length]) return path; 64 | return nil; 65 | } 66 | 67 | 68 | NSURL *MameWorkingDirectory(void) { 69 | 70 | NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; 71 | 72 | if ([defaults boolForKey: kUseCustomMame]) { 73 | NSString *path = [defaults stringForKey: kMameWorkingDirectory]; 74 | if (![path length]) return [NSURL fileURLWithPath: path]; 75 | } 76 | 77 | return SupportDirectory(); 78 | } 79 | 80 | NSString *MameWorkingDirectoryPath(void) { 81 | 82 | NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; 83 | 84 | if ([defaults boolForKey: kUseCustomMame]) { 85 | NSString *path = [defaults stringForKey: kMameWorkingDirectory]; 86 | if (![path length]) return path; 87 | } 88 | 89 | return SupportDirectoryPath(); 90 | } 91 | 92 | 93 | NSDictionary *MameMachine(NSString *machine) { 94 | static NSMutableDictionary *cache; 95 | 96 | if (!cache) cache = [NSMutableDictionary new]; 97 | NSDictionary *d; 98 | 99 | if (!machine) return nil; 100 | d = [cache objectForKey: machine]; 101 | if (d) return d; 102 | 103 | NSBundle *bundle = [NSBundle mainBundle]; 104 | NSURL *url= [bundle URLForResource: machine withExtension: @"plist"]; 105 | 106 | d = [NSDictionary dictionaryWithContentsOfURL: url]; 107 | if (d) [cache setObject: d forKey: machine]; 108 | return d; 109 | } 110 | 111 | /* NSCache doesn't retain it's key. This essentially interns it. */ 112 | /* could just abuse NSSelectorFromString() */ 113 | NSString *InternString(NSString *key) { 114 | static NSMutableSet *storage = nil; 115 | 116 | if (!storage) { 117 | storage = [NSMutableSet new]; 118 | } 119 | NSString *copy = [storage member: key]; 120 | if (!copy) { 121 | copy = [key copy]; 122 | [storage addObject: copy]; 123 | } 124 | return copy; 125 | } 126 | 127 | 128 | NSString *kUseCustomMame = @"UseCustomMame"; 129 | NSString *kMamePath = @"MamePath"; 130 | NSString *kMameWorkingDirectory = @"MameWorkingDirectory"; 131 | NSString *kAutoCloseLogWindow = @"AutoCloseLogWindow"; 132 | NSString *kUseLogWindow = @"UseLogWindow"; 133 | NSString *kMameComponentsDate = @"MameComponentsDate"; 134 | NSString *kDefaultDownloadURL = @"DefaultDownloadURL"; 135 | NSString *kDefaultDownloadExtension = @"DefaultDownloadExtension"; 136 | 137 | NSString *kDownloadURL = @"DownloadURL"; 138 | NSString *kDownloadExtension = @"DownloadExtension"; 139 | 140 | NSString *kNotificationDiskImageAdded = @"Disk Image Added"; 141 | NSString *kNotificationDiskImageMagicRoute = @"Disk Image Magic Route"; 142 | NSString *kNotificationBookmarkMagicRoute = @"Bookmark Magic Route"; 143 | -------------------------------------------------------------------------------- /Ample/AmpleLite.m: -------------------------------------------------------------------------------- 1 | // 2 | // AmpleLite.m 3 | // Ample Lite 4 | // 5 | // Created by Kelvin Sherlock on 6/14/2021. 6 | // Copyright © 2021 Kelvin Sherlock. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @class NSMenuItem; 12 | 13 | @interface SUUpdater : NSObject 14 | @end 15 | 16 | @implementation SUUpdater 17 | 18 | - (IBAction)checkForUpdates:(id)sender { 19 | } 20 | 21 | 22 | - (BOOL)validateMenuItem:(NSMenuItem *)menuItem { 23 | return NO; 24 | } 25 | 26 | @end 27 | -------------------------------------------------------------------------------- /Ample/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.h 3 | // Ample 4 | // 5 | // Created by Kelvin Sherlock on 8/16/2020. 6 | // Copyright © 2020 Kelvin Sherlock. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface AppDelegate : NSObject 12 | 13 | - (IBAction)displayPreferences:(id)sender; 14 | - (IBAction)manageBookmarks: (id)sender; 15 | @end 16 | 17 | -------------------------------------------------------------------------------- /Ample/AppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.m 3 | // Ample 4 | // 5 | // Created by Kelvin Sherlock on 8/16/2020. 6 | // Copyright © 2020 Kelvin Sherlock. All rights reserved. 7 | // 8 | #import "Ample.h" 9 | #import "AppDelegate.h" 10 | #import "LaunchWindowController.h" 11 | #import "PreferencesWindowController.h" 12 | #import "DownloadWindowController.h" 13 | #import "DiskImagesWindowController.h" 14 | #import "CheatSheetWindowController.h" 15 | #import "BookmarkWindowController.h" 16 | 17 | #import "Transformers.h" 18 | #import "BookmarkManager.h" 19 | 20 | #import "LogWindowController.h" 21 | 22 | @interface AppDelegate () 23 | @property (weak) IBOutlet NSWindow *installWindow; 24 | 25 | @end 26 | 27 | @implementation AppDelegate { 28 | NSWindowController *_prefs; 29 | NSWindowController *_launcher; 30 | NSWindowController *_downloader; 31 | NSWindowController *_diskImages; 32 | NSWindowController *_cheatSheet; 33 | NSWindowController *_bookmarks; 34 | 35 | } 36 | 37 | 38 | - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { 39 | // Insert code here to initialize your application 40 | 41 | NSBundle *bundle = [NSBundle mainBundle]; 42 | NSString *path; 43 | NSDictionary *dict; 44 | 45 | 46 | RegisterTransformers(); 47 | 48 | //BookmarkManager *bm = [BookmarkManager sharedManager]; 49 | 50 | path = [bundle pathForResource: @"Defaults" ofType: @"plist"]; 51 | dict = [NSDictionary dictionaryWithContentsOfFile: path]; 52 | 53 | if (dict) 54 | { 55 | [[NSUserDefaults standardUserDefaults] registerDefaults: dict]; 56 | } 57 | 58 | _diskImages = [DiskImagesWindowController sharedInstance]; //[DiskImagesWindowController new]; 59 | 60 | if ([self installMameComponents]) { 61 | 62 | [self displayLaunchWindow]; 63 | } 64 | } 65 | 66 | 67 | 68 | 69 | -(void)displayLaunchWindow { 70 | 71 | if (!_launcher) { 72 | _launcher = [LaunchWindowController new]; 73 | } 74 | [_launcher showWindow: nil]; 75 | } 76 | 77 | -(BOOL)installMameComponents { 78 | 79 | /* install the mame data components. */ 80 | NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; 81 | NSBundle *bundle = [NSBundle mainBundle]; 82 | NSURL *sd = SupportDirectory(); 83 | 84 | NSURL *ample_url = [sd URLByAppendingPathComponent: @"Ample.plist"]; 85 | NSMutableDictionary *d = [NSMutableDictionary dictionaryWithContentsOfURL: ample_url]; 86 | 87 | NSDate *oldDate = [d objectForKey: kMameComponentsDate]; 88 | NSDate *newDate = [defaults objectForKey: kMameComponentsDate]; 89 | if (![newDate isKindOfClass: [NSDate class]]) 90 | newDate = nil; 91 | 92 | /* oops, I accidentally called 2024-12-07 2025-12-07. So let's ignore future dates... */ 93 | NSDate *now = [NSDate dateWithTimeIntervalSinceNow: 0]; 94 | if (oldDate && [oldDate compare: now] >= 0) 95 | oldDate = nil; 96 | 97 | 98 | if (!newDate) return YES; //???? 99 | if (oldDate && [oldDate compare: newDate] >= 0) return YES; 100 | 101 | #if 0 102 | NSString *path = [bundle pathForResource: @"mame-data" ofType: @"tgz"]; 103 | if (!path) return YES; // Ample Lite? 104 | #endif 105 | 106 | NSString *ssp = [bundle sharedSupportPath]; 107 | NSString *path = [ssp stringByAppendingPathComponent: @"mame-data.tgz"]; 108 | if (![[NSFileManager defaultManager] fileExistsAtPath: path]) 109 | return YES; // Ample Lite? 110 | 111 | 112 | NSWindow *win = _installWindow; 113 | [win makeKeyAndOrderFront: nil]; 114 | NSTask *task = [NSTask new]; 115 | NSArray *argv = @[ 116 | @"xfz", 117 | path 118 | ]; 119 | if (@available(macOS 10.13, *)) { 120 | [task setExecutableURL: [NSURL fileURLWithPath: @"/usr/bin/tar"]]; 121 | [task setCurrentDirectoryURL: sd]; 122 | } else { 123 | [task setLaunchPath: @"/usr/bin/tar"]; 124 | [task setCurrentDirectoryPath: SupportDirectoryPath()]; 125 | } 126 | [task setArguments: argv]; 127 | 128 | 129 | dispatch_time_t when = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)); 130 | [task setTerminationHandler: ^(NSTask *task){ 131 | 132 | int st = [task terminationStatus]; 133 | // delay so the install window is visible, I think 134 | dispatch_after(when, dispatch_get_main_queue(), ^{ 135 | 136 | if (st) { 137 | NSAlert *alert = [NSAlert new]; 138 | [alert setMessageText: @"An error occurred extracting MAME components"]; 139 | [alert runModal]; 140 | [win close]; 141 | return; 142 | } 143 | 144 | if (d) { 145 | [d setObject: newDate forKey: kMameComponentsDate]; 146 | [d writeToURL: ample_url atomically: YES]; 147 | } else { 148 | [@{ kMameComponentsDate: newDate } writeToURL: ample_url atomically: YES]; 149 | } 150 | [win close]; 151 | [self displayLaunchWindow]; 152 | [self displayROMS: nil]; 153 | }); 154 | 155 | }]; 156 | [task launch]; 157 | 158 | return NO; 159 | } 160 | 161 | 162 | - (void)applicationWillTerminate:(NSNotification *)aNotification { 163 | // Insert code here to tear down your application 164 | 165 | } 166 | 167 | - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender { 168 | return YES; 169 | } 170 | 171 | 172 | -(BOOL)application:(NSApplication *)sender openFile:(NSString *)filename { 173 | 174 | NSString *ext = [[filename pathExtension] lowercaseString]; 175 | 176 | if ([ext isEqualToString: @"vgm"] || [ext isEqualToString: @"vgz"]) { 177 | // run mame... 178 | NSArray *args = @[ @"vgmplay", @"-window", @"-nomax", @"-skip_gameinfo", @"-quik", filename ]; 179 | 180 | [LogWindowController controllerForArgs: args]; 181 | } 182 | return NO; 183 | } 184 | 185 | 186 | #pragma mark - IBActions 187 | 188 | 189 | - (IBAction)displayPreferences:(id)sender { 190 | if (!_prefs) { 191 | _prefs = [PreferencesWindowController new]; 192 | } 193 | [_prefs showWindow: sender]; 194 | } 195 | 196 | 197 | - (IBAction)displayROMS:(id)sender { 198 | if (!_downloader) { 199 | _downloader = [DownloadWindowController sharedInstance]; 200 | } 201 | [_downloader showWindow: sender]; 202 | } 203 | 204 | - (IBAction)displayRecentDiskImages:(id)sender { 205 | if (!_diskImages) { 206 | _diskImages = [DiskImagesWindowController sharedInstance]; 207 | } 208 | [_diskImages showWindow: sender]; 209 | } 210 | 211 | - (IBAction)displayCheatSheet:(id)sender { 212 | if (!_cheatSheet) { 213 | _cheatSheet = [CheatSheetWindowController new]; 214 | } 215 | [_cheatSheet showWindow: sender]; 216 | } 217 | 218 | - (IBAction)displaySupportDirectory:(id)sender { 219 | NSURL *url = SupportDirectory(); 220 | NSWorkspace *ws = [NSWorkspace sharedWorkspace]; 221 | [ws openURL: url]; 222 | } 223 | - (IBAction)mameDocumentation:(id)sender { 224 | NSWorkspace *ws = [NSWorkspace sharedWorkspace]; 225 | 226 | NSURL *url = [NSURL URLWithString: @"https://docs.mamedev.org"]; 227 | [ws openURL: url]; 228 | } 229 | 230 | - (IBAction)mameAppleWiki:(id)sender { 231 | NSWorkspace *ws = [NSWorkspace sharedWorkspace]; 232 | 233 | NSURL *url = [NSURL URLWithString: @"https://wiki.mamedev.org/index.php/Driver:Apple_II"]; 234 | [ws openURL: url]; 235 | } 236 | 237 | - (IBAction)mameMac68kWiki:(id)sender { 238 | NSWorkspace *ws = [NSWorkspace sharedWorkspace]; 239 | 240 | NSURL *url = [NSURL URLWithString: @"https://wiki.mamedev.org/index.php/Driver:Mac_68K"]; 241 | [ws openURL: url]; 242 | } 243 | 244 | 245 | - (IBAction)manageBookmarks: (id)sender { 246 | 247 | if (!_bookmarks) { 248 | _bookmarks = [BookmarkWindowController new]; 249 | } 250 | [_bookmarks showWindow: sender]; 251 | } 252 | 253 | 254 | -(IBAction)installMameComponents:(id)sender { 255 | 256 | 257 | /* install the mame data components. */ 258 | NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; 259 | NSBundle *bundle = [NSBundle mainBundle]; 260 | NSURL *sd = SupportDirectory(); 261 | 262 | NSURL *ample_url = [sd URLByAppendingPathComponent: @"Ample.plist"]; 263 | NSMutableDictionary *d = [NSMutableDictionary dictionaryWithContentsOfURL: ample_url]; 264 | 265 | //NSDate *oldDate = [d objectForKey: kMameComponentsDate]; 266 | NSDate *newDate = [defaults objectForKey: kMameComponentsDate]; 267 | if (![newDate isKindOfClass: [NSDate class]]) 268 | newDate = nil; 269 | 270 | NSString *path = [bundle pathForResource: @"mame-data" ofType: @"tgz"]; 271 | if (!path) return; // Ample Lite? 272 | 273 | 274 | NSWindow *win = _installWindow; 275 | [win makeKeyAndOrderFront: nil]; 276 | NSTask *task = [NSTask new]; 277 | NSArray *argv = @[ 278 | @"xfz", 279 | path 280 | ]; 281 | if (@available(macOS 10.13, *)) { 282 | [task setExecutableURL: [NSURL fileURLWithPath: @"/usr/bin/tar"]]; 283 | [task setCurrentDirectoryURL: sd]; 284 | } else { 285 | [task setLaunchPath: @"/usr/bin/tar"]; 286 | [task setCurrentDirectoryPath: SupportDirectoryPath()]; 287 | } 288 | [task setArguments: argv]; 289 | 290 | 291 | //dispatch_time_t when = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)); 292 | [task setTerminationHandler: ^(NSTask *task){ 293 | 294 | int st = [task terminationStatus]; 295 | dispatch_async(dispatch_get_main_queue(), ^{ 296 | 297 | if (st) { 298 | NSAlert *alert = [NSAlert new]; 299 | [alert setMessageText: @"An error occurred extracting MAME components"]; 300 | [alert runModal]; 301 | [win close]; 302 | return; 303 | } 304 | 305 | if (d) { 306 | [d setObject: newDate forKey: kMameComponentsDate]; 307 | [d writeToURL: ample_url atomically: YES]; 308 | } else { 309 | [@{ kMameComponentsDate: newDate } writeToURL: ample_url atomically: YES]; 310 | } 311 | [win close]; 312 | // need to reload the software list data. 313 | }); 314 | 315 | }]; 316 | [task launch]; 317 | 318 | } 319 | 320 | @end 321 | -------------------------------------------------------------------------------- /Ample/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "icon-16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "icon-32.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "icon-32.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "icon-64.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "icon-128.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "icon-256.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "icon-256.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "icon-512.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "icon-512.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "icon-1024.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /Ample/Assets.xcassets/AppIcon.appiconset/icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ksherlock/ample/60cb69df42b436de43bcaa87a895c28212ee9757/Ample/Assets.xcassets/AppIcon.appiconset/icon-1024.png -------------------------------------------------------------------------------- /Ample/Assets.xcassets/AppIcon.appiconset/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ksherlock/ample/60cb69df42b436de43bcaa87a895c28212ee9757/Ample/Assets.xcassets/AppIcon.appiconset/icon-128.png -------------------------------------------------------------------------------- /Ample/Assets.xcassets/AppIcon.appiconset/icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ksherlock/ample/60cb69df42b436de43bcaa87a895c28212ee9757/Ample/Assets.xcassets/AppIcon.appiconset/icon-16.png -------------------------------------------------------------------------------- /Ample/Assets.xcassets/AppIcon.appiconset/icon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ksherlock/ample/60cb69df42b436de43bcaa87a895c28212ee9757/Ample/Assets.xcassets/AppIcon.appiconset/icon-256.png -------------------------------------------------------------------------------- /Ample/Assets.xcassets/AppIcon.appiconset/icon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ksherlock/ample/60cb69df42b436de43bcaa87a895c28212ee9757/Ample/Assets.xcassets/AppIcon.appiconset/icon-32.png -------------------------------------------------------------------------------- /Ample/Assets.xcassets/AppIcon.appiconset/icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ksherlock/ample/60cb69df42b436de43bcaa87a895c28212ee9757/Ample/Assets.xcassets/AppIcon.appiconset/icon-512.png -------------------------------------------------------------------------------- /Ample/Assets.xcassets/AppIcon.appiconset/icon-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ksherlock/ample/60cb69df42b436de43bcaa87a895c28212ee9757/Ample/Assets.xcassets/AppIcon.appiconset/icon-64.png -------------------------------------------------------------------------------- /Ample/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Ample/AutocompleteControl.h: -------------------------------------------------------------------------------- 1 | // 2 | // AutocompleteControl.h 3 | // Autocomplete 4 | // 5 | // Created by Kelvin Sherlock on 2/20/2021. 6 | // Copyright © 2021 Kelvin Sherlock. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @class AutocompleteControl; 14 | 15 | @protocol AutocompleteItem 16 | -(NSString *)menuTitle; 17 | -(NSAttributedString *)menuAttributedTitle; //?? can it still handle color? 18 | -(BOOL)menuEnabled; 19 | -(BOOL)menuIsHeader; 20 | @end 21 | 22 | 23 | @protocol AutoCompleteDelegate 24 | 25 | -(NSArray> *)autocomplete: (AutocompleteControl *)control completionsForString: (NSString *)string; 26 | -(NSArray> *)autocomplete: (AutocompleteControl *)control completionsForItem: (id)item; 27 | 28 | @end 29 | 30 | @interface AutocompleteControl : NSSearchField 31 | 32 | @property NSInteger minWidth; 33 | @property NSInteger maxDisplayItems; 34 | @property (nullable, weak) id autocompleteDelegate; 35 | 36 | -(void)invalidate; 37 | 38 | 39 | @end 40 | 41 | 42 | NS_ASSUME_NONNULL_END 43 | -------------------------------------------------------------------------------- /Ample/Base.lproj/Autocomplete.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 45 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /Ample/Base.lproj/CheatSheet.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /Ample/Base.lproj/Credits.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\ansi\ansicpg1252\cocoartf1671\cocoasubrtf600 2 | {\fonttbl\f0\fswiss\fcharset0 Helvetica-Bold;\f1\fswiss\fcharset0 Helvetica;} 3 | {\colortbl;\red255\green255\blue255;\red0\green0\blue0;\red255\green255\blue255;\red38\green38\blue38; 4 | } 5 | {\*\expandedcolortbl;;\cssrgb\c0\c0\c0;\cssrgb\c100000\c100000\c100000;\cssrgb\c20000\c20000\c20000; 6 | } 7 | \margl1440\margr1440\vieww9000\viewh8400\viewkind0 8 | \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 9 | 10 | \f0\b\fs36 \cf2 Ample\cf0 11 | \f1\b0\fs28 would like to thank\'85\ 12 | \ 13 | \pard\pardeftab720\qc\partightenfactor0 14 | {\field{\*\fldinst{HYPERLINK "https://www.mamedev.org"}}{\fldrslt 15 | \f0\b\fs36 \cf0 \cb3 \expnd0\expndtw0\kerning0 16 | MAME}}\cb3 \expnd0\expndtw0\kerning0 17 | \ 18 | \pard\pardeftab720\partightenfactor0 19 | \cf0 \ 20 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qj\partightenfactor0 21 | \cf0 \cb1 \kerning1\expnd0\expndtw0 The MAME project as a whole is distributed under the terms of the {\field{\*\fldinst{HYPERLINK "https://opensource.org/licenses/GPL-2.0"}}{\fldrslt GNU General Public License, 2}} (GPL-2.0), since it contains code made available under multiple GPL-compatible licenses. A great majority of files (over 90% including core files) are under the {\field{\*\fldinst{HYPERLINK "https://opensource.org/licenses/BSD-3-Clause"}}{\fldrslt BSD-3-Clause License}} and we would encourage new contributors to distribute files under this license.\ 22 | \ 23 | Please note that MAME is a registered trademark of Gregory Ember, and permission is required to use the "MAME" name, logo or wordmark.\cb3 \expnd0\expndtw0\kerning0 24 | \ 25 | \pard\pardeftab720\partightenfactor0 26 | \cf0 \ 27 | \pard\pardeftab720\qc\partightenfactor0 28 | {\field{\*\fldinst{HYPERLINK "https://libsdl.org"}}{\fldrslt 29 | \f0\b\fs36 \cf0 SDL}}\ 30 | \pard\pardeftab720\partightenfactor0 31 | \cf0 \ 32 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qj\partightenfactor0 33 | \cf0 \cb1 \kerning1\expnd0\expndtw0 SDL 2.0 is distributed under the {\field{\*\fldinst{HYPERLINK "https://libsdl.org/license.php"}}{\fldrslt zlib license}}. This license allows you to use SDL freely in any software.\ 34 | \ 35 | \pard\pardeftab720\qc\partightenfactor0 36 | {\field{\*\fldinst{HYPERLINK "https://sparkle-project.org/"}}{\fldrslt 37 | \f0\b\fs36 \cf0 \cb3 \expnd0\expndtw0\kerning0 38 | Sparkle}}\ 39 | \pard\pardeftab720\partightenfactor0 40 | \cf0 \ 41 | Sparkle is open source software available under the permissive MIT license, and is developed on GitHub by the Sparkle Project with the help of dozens of valued contributors.\ 42 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\partightenfactor0 43 | \cf0 \ 44 | \pard\pardeftab720\qc\partightenfactor0 45 | {\field{\*\fldinst{HYPERLINK "https://usdawatercolors.nal.usda.gov/pom/catalog.xhtml?id=POM00001916"}}{\fldrslt 46 | \f0\b\fs36 \cf0 \cb3 \expnd0\expndtw0\kerning0 47 | Icon}} 48 | \f0\b\fs36 \cb3 \expnd0\expndtw0\kerning0 49 | \ 50 | \pard\pardeftab720\qj\partightenfactor0 51 | 52 | \f1\b0\fs28 \cf0 \ 53 | \pard\pardeftab720\qj\partightenfactor0 54 | \cf4 R.C. Steadman, 1921-06-01.\ 55 | \ 56 | U.S. Department of Agriculture Pomological Watercolor Collection. Rare and Special Collections, National Agricultural Library, Beltsville, MD 20705} -------------------------------------------------------------------------------- /Ample/Base.lproj/LogWindow.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /Ample/Base.lproj/MachineView.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 102 | 106 | 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /Ample/Bookmark.h: -------------------------------------------------------------------------------- 1 | // 2 | // Bookmark.h 3 | // Ample 4 | // 5 | // Created by Kelvin Sherlock on 2/7/2022. 6 | // Copyright © 2022 Kelvin Sherlock. All rights reserved. 7 | // 8 | 9 | #ifndef Bookmark_h 10 | #define Bookmark_h 11 | 12 | #import "Bookmark+CoreDataClass.h" 13 | 14 | #endif /* Bookmark_h */ 15 | -------------------------------------------------------------------------------- /Ample/BookmarkManager.h: -------------------------------------------------------------------------------- 1 | // 2 | // BookmarkManager.h 3 | // Ample 4 | // 5 | // Created by Kelvin Sherlock on 6/1/2021. 6 | // Copyright © 2021 Kelvin Sherlock. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | 12 | @class NSMenu; 13 | @class NSMenuItem; 14 | @class Bookmark; 15 | @class DiskImage; 16 | 17 | NS_ASSUME_NONNULL_BEGIN 18 | 19 | @interface BookmarkManager : NSObject 20 | 21 | @property (weak) IBOutlet NSMenu *menu; 22 | @property (weak) IBOutlet NSMenuItem *updateMenuItem; 23 | @property (readonly) NSManagedObjectContext *managedObjectContext; 24 | 25 | @property (nullable) Bookmark *currentBookmark; 26 | 27 | +(instancetype)sharedManager; 28 | 29 | -(NSString *)uniqueBookmarkName: (NSString *)name; 30 | 31 | -(NSError *)saveBookmark: (NSDictionary *)bookmark name: (NSString *)name automatic: (BOOL)automatic; 32 | 33 | //-(NSError *)saveDefault: (NSDictionary *)bookmark; 34 | 35 | -(Bookmark *)defaultBookmark; 36 | -(NSDictionary *)loadDefault; 37 | 38 | -(NSError *)setAutomatic: (Bookmark *)bookmark; 39 | 40 | -(BOOL)addDiskImage: (NSObject *)pathOrURL; 41 | 42 | //-(void)convertLegacyBookmarks; 43 | @end 44 | 45 | NS_ASSUME_NONNULL_END 46 | -------------------------------------------------------------------------------- /Ample/BookmarkWindowController.h: -------------------------------------------------------------------------------- 1 | // 2 | // BookmarkWindowController.h 3 | // Ample 4 | // 5 | // Created by Kelvin Sherlock on 2/6/2022. 6 | // Copyright © 2022 Kelvin Sherlock. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface BookmarkWindowController : NSWindowController 14 | 15 | +(instancetype)sharedInstance; 16 | 17 | @end 18 | 19 | NS_ASSUME_NONNULL_END 20 | -------------------------------------------------------------------------------- /Ample/BookmarkWindowController.m: -------------------------------------------------------------------------------- 1 | // 2 | // BookmarkWindowController.m 3 | // Ample 4 | // 5 | // Created by Kelvin Sherlock on 2/6/2022. 6 | // Copyright © 2022 Kelvin Sherlock. All rights reserved. 7 | // 8 | 9 | #import "BookmarkWindowController.h" 10 | #import "BookmarkManager.h" 11 | #import "Bookmark.h" 12 | #import "Ample.h" 13 | 14 | 15 | 16 | @interface BookmarkWindowController () 17 | @property (strong) IBOutlet NSArrayController *arrayController; 18 | @property (strong) IBOutlet BookmarkManager *bookmarkManager; 19 | 20 | @property (weak) IBOutlet NSTableView *tableView; 21 | 22 | @end 23 | 24 | @implementation BookmarkWindowController 25 | 26 | 27 | +(instancetype)sharedInstance { 28 | static BookmarkWindowController *me = nil; 29 | if (!me) { 30 | me = [self new]; 31 | } 32 | return me; 33 | } 34 | 35 | -(NSString *)windowNibName { 36 | return @"BookmarkWindow"; 37 | } 38 | 39 | 40 | - (void)windowDidLoad { 41 | [super windowDidLoad]; 42 | 43 | NSSortDescriptor *s = [NSSortDescriptor sortDescriptorWithKey: @"name" ascending: YES selector: @selector(caseInsensitiveCompare:)]; 44 | 45 | [_arrayController setSortDescriptors: @[s]]; 46 | 47 | } 48 | 49 | -(BOOL)windowShouldClose: (NSWindow *)sender { 50 | 51 | NSManagedObjectContext *moc = [_arrayController managedObjectContext]; 52 | NSError *error; 53 | 54 | if (![_arrayController commitEditing]) return NO; 55 | 56 | if ([moc save: &error]) return YES; 57 | NSLog(@"%@", error); 58 | 59 | #if 0 60 | NSDictionary *dict = [error userInfo]; 61 | NSArray *array = [dict objectForKey: @"conflictList"]; 62 | for (NSConstraintConflict *c in array) { 63 | 64 | NSArray * arr = [c conflictingObjects]; 65 | for (NSManagedObject *o in arr) { 66 | 67 | } 68 | } 69 | #endif 70 | return YES; 71 | 72 | //[self presentError: error]; 73 | //return NO; 74 | } 75 | 76 | -(void)keyDown:(NSEvent *)event { 77 | /* Carbon/Events.h */ 78 | enum { 79 | kVK_Delete = 0x33, 80 | kVK_ForwardDelete = 0x75, 81 | 82 | }; 83 | unsigned short keyCode = [event keyCode]; 84 | 85 | if (keyCode == kVK_Delete || keyCode == kVK_ForwardDelete) { 86 | 87 | // arraycontroller selected object / selected index doesn't work right. 88 | 89 | NSInteger row = [_tableView selectedRow]; 90 | if (row >= 0) 91 | [_arrayController removeObjectAtArrangedObjectIndex: row]; 92 | 93 | } 94 | } 95 | 96 | -(Bookmark *)clickedItem { 97 | 98 | NSArray *array = [_arrayController arrangedObjects]; 99 | NSInteger row = [_tableView clickedRow]; 100 | if (row < 0 || row >= [array count]) return nil; 101 | return [array objectAtIndex: row]; 102 | } 103 | 104 | -(IBAction)doubleClick:(id)sender { 105 | 106 | Bookmark *b = [self clickedItem]; 107 | if (!b) return; 108 | 109 | NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; 110 | 111 | [nc postNotificationName: kNotificationBookmarkMagicRoute object: b]; 112 | } 113 | 114 | -(IBAction)toggleDefault:(id)sender { 115 | NSLog(@"%@", sender); 116 | 117 | 118 | } 119 | 120 | -(IBAction)setDefault:(id)sender { 121 | 122 | Bookmark *b = [self clickedItem]; 123 | if (!b) return; 124 | 125 | [_bookmarkManager setAutomatic: b]; 126 | } 127 | 128 | -(IBAction)clearDefault:(id)sender { 129 | 130 | Bookmark *b = [self clickedItem]; 131 | if (!b) return; 132 | 133 | [b setAutomatic: NO]; 134 | } 135 | 136 | 137 | -(IBAction)deleteBookmark:(id)sender { 138 | 139 | //Bookmark *b = [self clickedItem]; 140 | //if (!b) return; 141 | 142 | NSInteger row = [_tableView clickedRow]; 143 | if (row >= 0) 144 | [_arrayController removeObjectAtArrangedObjectIndex: row]; 145 | } 146 | 147 | @end 148 | 149 | @implementation BookmarkWindowController (Menu) 150 | 151 | -(BOOL)validateMenuItem:(NSMenuItem *)menuItem { 152 | 153 | Bookmark *b = [self clickedItem]; 154 | 155 | if (!b) return NO; 156 | SEL action = [menuItem action]; 157 | 158 | if (action == @selector(clearDefault:)) { 159 | return [b automatic]; 160 | } 161 | 162 | if (action == @selector(setDefault:)) { 163 | return ![b automatic]; 164 | } 165 | 166 | 167 | return YES; 168 | } 169 | 170 | @end 171 | -------------------------------------------------------------------------------- /Ample/CheatSheetWindowController.h: -------------------------------------------------------------------------------- 1 | // 2 | // CheatSheetWindowController.h 3 | // Ample 4 | // 5 | // Created by Kelvin Sherlock on 1/14/2021. 6 | // Copyright © 2021 Kelvin Sherlock. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface CheatSheetWindowController : NSWindowController 14 | 15 | @end 16 | 17 | NS_ASSUME_NONNULL_END 18 | -------------------------------------------------------------------------------- /Ample/CheatSheetWindowController.m: -------------------------------------------------------------------------------- 1 | // 2 | // CheatSheetWindowController.m 3 | // Ample 4 | // 5 | // Created by Kelvin Sherlock on 1/14/2021. 6 | // Copyright © 2021 Kelvin Sherlock. All rights reserved. 7 | // 8 | 9 | #import "CheatSheetWindowController.h" 10 | 11 | #import 12 | 13 | @interface CheatSheetWindowController () 14 | @property (weak) IBOutlet WKWebView *webView; 15 | 16 | @end 17 | 18 | @interface CheatSheetWindowController (NavigationDelegate) 19 | @end 20 | 21 | @implementation CheatSheetWindowController 22 | 23 | -(NSString *)windowNibName { 24 | return @"CheatSheet"; 25 | } 26 | 27 | - (void)windowDidLoad { 28 | [super windowDidLoad]; 29 | 30 | if (!_webView) { 31 | WKWebView *webView; 32 | NSWindow *window = [self window]; 33 | CGRect frame = [[window contentView] frame]; 34 | 35 | 36 | webView = [WKWebView new]; 37 | [webView setFrame: frame]; 38 | [webView setNavigationDelegate: self]; 39 | [[window contentView]addSubview: webView]; 40 | _webView = webView; 41 | } 42 | 43 | 44 | [_webView setHidden: YES]; 45 | NSBundle *bundle = [NSBundle mainBundle]; 46 | NSURL *url = [bundle URLForResource: @"CheatSheet" withExtension: @"html"]; 47 | //[[[_webView configuration] preferences] setValue: @YES forKey: @"developerExtrasEnabled"]; 48 | [_webView loadFileURL: url allowingReadAccessToURL: url]; 49 | 50 | } 51 | 52 | -(void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation { 53 | // delay to prevent flash in dark mode. 54 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0/8 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ 55 | [webView setHidden: NO]; 56 | }); 57 | 58 | } 59 | 60 | 61 | @end 62 | -------------------------------------------------------------------------------- /Ample/Core Data/Ample.xcdatamodeld/Ample.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Ample/Core Data/Bookmark+CoreDataClass.h: -------------------------------------------------------------------------------- 1 | // 2 | // Bookmark+CoreDataClass.h 3 | // Ample 4 | // 5 | // Created by Kelvin Sherlock on 2/6/2022. 6 | // Copyright © 2022 Kelvin Sherlock. All rights reserved. 7 | // 8 | // 9 | 10 | #import 11 | #import 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | 15 | @interface Bookmark : NSManagedObject 16 | 17 | @property NSDictionary *dictionary; 18 | 19 | +(NSString *)uniqueName: (NSString *)name inContext: (NSManagedObjectContext *)context; 20 | 21 | @end 22 | 23 | NS_ASSUME_NONNULL_END 24 | 25 | #import "Bookmark+CoreDataProperties.h" 26 | -------------------------------------------------------------------------------- /Ample/Core Data/Bookmark+CoreDataClass.m: -------------------------------------------------------------------------------- 1 | // 2 | // Bookmark+CoreDataClass.m 3 | // Ample 4 | // 5 | // Created by Kelvin Sherlock on 2/6/2022. 6 | // Copyright © 2022 Kelvin Sherlock. All rights reserved. 7 | // 8 | // 9 | 10 | #import "Bookmark+CoreDataClass.h" 11 | 12 | @implementation Bookmark 13 | 14 | /* extract the number from a trailing " (%d)" */ 15 | static int extract_number(NSString *s, NSInteger offset) { 16 | 17 | unichar buffer[32]; 18 | NSInteger len = [s length] - offset; 19 | unichar c; 20 | int i; 21 | int n = 0; 22 | 23 | if (len < 4) return -1; /* " (1)"*/ 24 | if (len > 6) return -1; /* " (999)" */ 25 | 26 | NSRange r = NSMakeRange(offset, len); 27 | [s getCharacters: buffer range: r]; 28 | 29 | buffer[len] = 0; 30 | i = 0; 31 | if (buffer[i++] != ' ') return -1; 32 | if (buffer[i++] != '(') return -1; 33 | 34 | c = buffer[i++]; 35 | if (c < '1' || c > '9') return -1; 36 | n = c - '0'; 37 | 38 | for (;;) { 39 | c = buffer[i]; 40 | if (c < '0' || c > '9') break; 41 | n = n * 10 + (c - '0'); 42 | ++i; 43 | } 44 | 45 | if (buffer[i++] != ')') return -1; 46 | if (buffer[i++] != 0) return -1; 47 | 48 | return n; 49 | } 50 | 51 | +(NSString *)uniqueName: (NSString *)name inContext: (NSManagedObjectContext *)context { 52 | 53 | NSInteger length = [name length]; 54 | 55 | NSError *error = nil; 56 | NSPredicate *p = [NSPredicate predicateWithFormat: @"name BEGINSWITH %@", name]; 57 | NSFetchRequest *req = [NSFetchRequest fetchRequestWithEntityName: @"Bookmark"]; 58 | [req setPredicate: p]; 59 | 60 | NSArray *array = [context executeFetchRequest: req error: &error]; 61 | if (![array count]) return name; 62 | 63 | uint64_t bits = 1; /* mark 0 as unavailable */ 64 | NSInteger max = 0; 65 | BOOL exact = NO; 66 | for (Bookmark *b in array) { 67 | NSString *s = [b name]; 68 | if ([name isEqualToString: s]) { 69 | exact = YES; 70 | continue; 71 | } 72 | int n = extract_number(s, length); 73 | if (n < 1) continue; 74 | if (n > max) max = n; 75 | if (n < 64) 76 | bits |= (1 << n); 77 | } 78 | if (!exact) return name; 79 | 80 | if (bits == (uint64_t)-1) { 81 | if (max == 999) return nil; 82 | return [name stringByAppendingFormat: @" (%u)", (int)(max + 1)]; 83 | } 84 | 85 | #if 1 86 | int ix = 0; 87 | while (bits) { 88 | ++ix; 89 | bits >>= 1; 90 | } 91 | #else 92 | // this doesn't work correctly. 93 | int ix = __builtin_ffsll(~bits); 94 | #endif 95 | return [name stringByAppendingFormat: @" (%u)", ix]; 96 | 97 | } 98 | 99 | -(void)setDictionary:(NSDictionary *)dictionary { 100 | 101 | NSData *data; 102 | NSError *error = nil; 103 | 104 | data = [NSPropertyListSerialization dataWithPropertyList: dictionary 105 | format: NSPropertyListBinaryFormat_v1_0 106 | options: 0 107 | error: &error]; 108 | 109 | [self setData: data]; 110 | } 111 | 112 | -(NSDictionary *)dictionary { 113 | 114 | // NSDictionary *dict; 115 | NSData *data = [self data]; 116 | NSError *error = nil; 117 | 118 | return [NSPropertyListSerialization propertyListWithData: data 119 | options: 0 120 | format: nil 121 | error: &error]; 122 | } 123 | 124 | 125 | - (NSError *)errorFromOriginalError:(NSError *)originalError error:(NSError*)secondError 126 | { 127 | NSMutableDictionary *userInfo = [NSMutableDictionary dictionary]; 128 | NSMutableArray *errors = [NSMutableArray arrayWithObject:secondError]; 129 | if ([originalError code] == NSValidationMultipleErrorsError) { 130 | [userInfo addEntriesFromDictionary:[originalError userInfo]]; 131 | [errors addObjectsFromArray:[userInfo objectForKey:NSDetailedErrorsKey]]; 132 | } else { 133 | [errors addObject:originalError]; 134 | } 135 | [userInfo setObject:errors forKey:NSDetailedErrorsKey]; 136 | return [NSError errorWithDomain:NSCocoaErrorDomain code:NSValidationMultipleErrorsError userInfo:userInfo]; 137 | } 138 | 139 | - (BOOL)validateName:(id*)ioValue error:(NSError**)outError { 140 | 141 | if (!ioValue || !*ioValue) return YES; 142 | NSString *name = *ioValue; 143 | 144 | NSFetchRequest *frq = [NSFetchRequest fetchRequestWithEntityName: @"Bookmark"]; 145 | 146 | NSPredicate *p = [NSPredicate predicateWithFormat: @"name = %@", name]; 147 | [frq setPredicate: p]; 148 | 149 | NSArray * arr = [[self managedObjectContext] executeFetchRequest: frq error: nil]; 150 | BOOL dupe = NO; 151 | for (Bookmark *b in arr) { 152 | if (b == self) continue; 153 | dupe = YES; 154 | break; 155 | } 156 | if (dupe && outError) { 157 | NSDictionary *dict = @{ NSLocalizedFailureReasonErrorKey: @"duplicate name", 158 | NSLocalizedDescriptionKey: @"duplicate name", 159 | NSValidationKeyErrorKey: @"name", 160 | NSValidationValueErrorKey: name, 161 | NSValidationObjectErrorKey: self 162 | }; 163 | NSError *e = [NSError errorWithDomain: @"Ample" code: 1 userInfo: dict]; 164 | 165 | if (*outError) { 166 | *outError = [self errorFromOriginalError: *outError error: e]; 167 | } else { 168 | *outError = e; 169 | } 170 | } 171 | return !dupe; 172 | } 173 | 174 | 175 | @end 176 | -------------------------------------------------------------------------------- /Ample/Core Data/Bookmark+CoreDataProperties.h: -------------------------------------------------------------------------------- 1 | // 2 | // Bookmark+CoreDataProperties.h 3 | // Ample 4 | // 5 | // Created by Kelvin Sherlock on 2/6/2022. 6 | // Copyright © 2022 Kelvin Sherlock. All rights reserved. 7 | // 8 | // 9 | 10 | #import "Bookmark+CoreDataClass.h" 11 | 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | 15 | @interface Bookmark (CoreDataProperties) 16 | 17 | + (NSFetchRequest *)fetchRequest; 18 | 19 | @property (nullable, nonatomic, copy) NSString *name; 20 | @property (nullable, nonatomic, copy) NSString *machine; 21 | @property (nullable, nonatomic, retain) NSData *data; 22 | @property (nullable, nonatomic, copy) NSDate *created; 23 | @property (nullable, nonatomic, copy) NSDate *modified; 24 | @property (nullable, nonatomic, copy) NSString *comment; 25 | @property (nonatomic) BOOL automatic; 26 | @end 27 | 28 | NS_ASSUME_NONNULL_END 29 | -------------------------------------------------------------------------------- /Ample/Core Data/Bookmark+CoreDataProperties.m: -------------------------------------------------------------------------------- 1 | // 2 | // Bookmark+CoreDataProperties.m 3 | // Ample 4 | // 5 | // Created by Kelvin Sherlock on 2/6/2022. 6 | // Copyright © 2022 Kelvin Sherlock. All rights reserved. 7 | // 8 | // 9 | 10 | #import "Bookmark+CoreDataProperties.h" 11 | 12 | @implementation Bookmark (CoreDataProperties) 13 | 14 | + (NSFetchRequest *)fetchRequest { 15 | return [NSFetchRequest fetchRequestWithEntityName:@"Bookmark"]; 16 | } 17 | 18 | @dynamic name; 19 | @dynamic machine; 20 | @dynamic data; 21 | @dynamic created; 22 | @dynamic modified; 23 | @dynamic comment; 24 | @dynamic automatic; 25 | 26 | @end 27 | -------------------------------------------------------------------------------- /Ample/Core Data/DiskImage+CoreDataClass.h: -------------------------------------------------------------------------------- 1 | // 2 | // DiskImage+CoreDataClass.h 3 | // Ample 4 | // 5 | // Created by Kelvin Sherlock on 2/7/2022. 6 | // Copyright © 2022 Kelvin Sherlock. All rights reserved. 7 | // 8 | // 9 | 10 | #import 11 | #import 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | 15 | @interface DiskImage : NSManagedObject 16 | 17 | @end 18 | 19 | NS_ASSUME_NONNULL_END 20 | 21 | #import "DiskImage+CoreDataProperties.h" 22 | -------------------------------------------------------------------------------- /Ample/Core Data/DiskImage+CoreDataClass.m: -------------------------------------------------------------------------------- 1 | // 2 | // DiskImage+CoreDataClass.m 3 | // Ample 4 | // 5 | // Created by Kelvin Sherlock on 2/7/2022. 6 | // Copyright © 2022 Kelvin Sherlock. All rights reserved. 7 | // 8 | // 9 | 10 | #import "DiskImage+CoreDataClass.h" 11 | 12 | @implementation DiskImage 13 | 14 | @end 15 | -------------------------------------------------------------------------------- /Ample/Core Data/DiskImage+CoreDataProperties.h: -------------------------------------------------------------------------------- 1 | // 2 | // DiskImage+CoreDataProperties.h 3 | // Ample 4 | // 5 | // Created by Kelvin Sherlock on 2/7/2022. 6 | // Copyright © 2022 Kelvin Sherlock. All rights reserved. 7 | // 8 | // 9 | 10 | #import "DiskImage+CoreDataClass.h" 11 | 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | 15 | @interface DiskImage (CoreDataProperties) 16 | 17 | + (NSFetchRequest *)fetchRequest; 18 | 19 | @property (nullable, nonatomic, copy) NSString *path; 20 | @property (nullable, nonatomic, copy) NSDate *added; 21 | @property (nonatomic) int64_t size; 22 | @property (nullable, nonatomic, copy) NSDate *accessed; 23 | @property (nullable, nonatomic, copy) NSString *name; 24 | 25 | -(void)updatePath; 26 | 27 | @end 28 | 29 | NS_ASSUME_NONNULL_END 30 | -------------------------------------------------------------------------------- /Ample/Core Data/DiskImage+CoreDataProperties.m: -------------------------------------------------------------------------------- 1 | // 2 | // DiskImage+CoreDataProperties.m 3 | // Ample 4 | // 5 | // Created by Kelvin Sherlock on 2/7/2022. 6 | // Copyright © 2022 Kelvin Sherlock. All rights reserved. 7 | // 8 | // 9 | 10 | #import "DiskImage+CoreDataProperties.h" 11 | 12 | #if 0 13 | @interface DiskImage () { 14 | NSString *_name; 15 | } 16 | @end 17 | #endif 18 | 19 | @implementation DiskImage (CoreDataProperties) 20 | 21 | + (NSFetchRequest *)fetchRequest { 22 | return [NSFetchRequest fetchRequestWithEntityName:@"DiskImage"]; 23 | } 24 | 25 | @dynamic path; 26 | @dynamic added; 27 | @dynamic size; 28 | @dynamic accessed; 29 | @dynamic name; 30 | 31 | -(void)updatePath { 32 | 33 | NSString *path = [self primitiveValueForKey: @"path"]; 34 | [self setName: [path lastPathComponent]]; 35 | } 36 | 37 | -(void)awakeFromFetch { 38 | [super awakeFromFetch]; 39 | 40 | [self updatePath]; 41 | } 42 | 43 | #if 0 44 | -(void)awakeFromInsert { 45 | [super awakeFromInsert]; 46 | 47 | NSString *path = [self primitiveValueForKey: @"path"]; 48 | [self setName: [path lastPathComponent]]; 49 | } 50 | #endif 51 | 52 | @end 53 | -------------------------------------------------------------------------------- /Ample/Defaults.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | MameComponentsDate 6 | 2025-05-01T12:00:00Z 7 | UseCustomMame 8 | 9 | AutoCloseLogWindow 10 | 11 | MamePath 12 | /usr/local/bin/mame 13 | MameWorkingDirectory 14 | /usr/local/share/mame/ 15 | NSQuitAlwaysKeepsWindows 16 | 17 | DefaultDownloadURL 18 | https://www.callapple.org/roms/ 19 | UseLogWindow 20 | 21 | DefaultDownloadExtension 22 | zip 23 | 24 | 25 | -------------------------------------------------------------------------------- /Ample/DiskImage.h: -------------------------------------------------------------------------------- 1 | // 2 | // DiskImage.h 3 | // Ample 4 | // 5 | // Created by Kelvin Sherlock on 2/7/2022. 6 | // Copyright © 2022 Kelvin Sherlock. All rights reserved. 7 | // 8 | 9 | #ifndef DiskImage_h 10 | #define DiskImage_h 11 | 12 | #import "DiskImage+CoreDataClass.h" 13 | 14 | #endif /* DiskImage_h */ 15 | -------------------------------------------------------------------------------- /Ample/DiskImagesWindowController.h: -------------------------------------------------------------------------------- 1 | // 2 | // DiskImagesWindowController.h 3 | // Ample 4 | // 5 | // Created by Kelvin Sherlock on 9/13/2020. 6 | // Copyright © 2020 Kelvin Sherlock. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface DiskImagesWindowController : NSWindowController 14 | 15 | +(instancetype)sharedInstance; 16 | 17 | @end 18 | 19 | @interface DiskImagesWindowController (TableView) 20 | 21 | @end 22 | 23 | @interface DiskImagesWindowController (Menu) 24 | 25 | @end 26 | 27 | 28 | NS_ASSUME_NONNULL_END 29 | -------------------------------------------------------------------------------- /Ample/DownloadWindowController.h: -------------------------------------------------------------------------------- 1 | // 2 | // DownloadWindowController.h 3 | // Ample 4 | // 5 | // Created by Kelvin Sherlock on 9/2/2020. 6 | // Copyright © 2020 Kelvin Sherlock. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface DownloadWindowController : NSWindowController 14 | 15 | @property NSString *currentROM; 16 | @property NSInteger currentCount; 17 | @property NSInteger totalCount; 18 | @property NSInteger errorCount; 19 | @property BOOL active; 20 | 21 | +(instancetype)sharedInstance; 22 | 23 | 24 | @end 25 | 26 | @interface DownloadWindowController (URL) 27 | @end 28 | 29 | 30 | @interface DownloadWindowController (Menu) 31 | 32 | @end 33 | 34 | NS_ASSUME_NONNULL_END 35 | -------------------------------------------------------------------------------- /Ample/EjectButton.h: -------------------------------------------------------------------------------- 1 | // 2 | // EjectButton.h 3 | // Ample 4 | // 5 | // Created by Kelvin Sherlock on 9/7/2020. 6 | // Copyright © 2020 Kelvin Sherlock. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface EjectButton : NSButton 14 | 15 | @end 16 | 17 | NS_ASSUME_NONNULL_END 18 | -------------------------------------------------------------------------------- /Ample/EjectButton.m: -------------------------------------------------------------------------------- 1 | // 2 | // EjectButton.m 3 | // Ample 4 | // 5 | // Created by Kelvin Sherlock on 9/7/2020. 6 | // Copyright © 2020 Kelvin Sherlock. All rights reserved. 7 | // 8 | 9 | #import "EjectButton.h" 10 | 11 | static NSImage *ejectImage = nil; 12 | static NSImage *ejectHoverImage = nil; 13 | 14 | @implementation EjectButton { 15 | NSTrackingRectTag _tracking; 16 | BOOL _mouse; 17 | } 18 | 19 | +(void)initialize { 20 | // content tint only works with template images. 21 | ejectImage = [NSImage imageNamed: @"eject-16x16"]; 22 | ejectHoverImage = [NSImage imageNamed: @"eject-hover-16x16"]; 23 | [ejectImage setTemplate: YES]; 24 | [ejectHoverImage setTemplate: YES]; 25 | } 26 | 27 | -(void)awakeFromNib { 28 | [super awakeFromNib]; 29 | [self setButtonType: NSButtonTypeMomentaryPushIn]; 30 | [self setImage: ejectImage]; 31 | [self setAlternateImage: ejectHoverImage]; 32 | // _tracking = [self addTrackingRect: [self bounds] owner: self userData: nil assumeInside: NO]; 33 | } 34 | 35 | -(void)viewDidMoveToWindow { 36 | if (!_tracking) 37 | _tracking = [self addTrackingRect: [self bounds] owner: self userData: nil assumeInside: NO]; 38 | 39 | } 40 | 41 | -(void)mouseEntered:(NSEvent *)event { 42 | _mouse = YES; 43 | if ([self isEnabled]) 44 | [self setImage: ejectHoverImage]; 45 | } 46 | -(void)mouseExited:(NSEvent *)event { 47 | _mouse = NO; 48 | if ([self isEnabled]) 49 | [self setImage: ejectImage]; 50 | } 51 | 52 | -(void)setEnabled:(BOOL)enabled { 53 | [super setEnabled: enabled]; 54 | if (_mouse) { 55 | [self setImage: enabled ? ejectHoverImage : ejectImage]; 56 | } 57 | } 58 | 59 | 60 | @end 61 | -------------------------------------------------------------------------------- /Ample/FlippedView.h: -------------------------------------------------------------------------------- 1 | // 2 | // FlippedView.h 3 | // Ample 4 | // 5 | // Created by Kelvin Sherlock on 8/19/2020. 6 | // Copyright © 2020 Kelvin Sherlock. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface FlippedView : NSView 14 | 15 | @end 16 | 17 | NS_ASSUME_NONNULL_END 18 | -------------------------------------------------------------------------------- /Ample/FlippedView.m: -------------------------------------------------------------------------------- 1 | // 2 | // FlippedView.m 3 | // Ample 4 | // 5 | // Created by Kelvin Sherlock on 8/19/2020. 6 | // Copyright © 2020 Kelvin Sherlock. All rights reserved. 7 | // 8 | 9 | #import "FlippedView.h" 10 | 11 | @implementation FlippedView 12 | 13 | -(BOOL)isFlipped { 14 | return YES; 15 | } 16 | 17 | @end 18 | -------------------------------------------------------------------------------- /Ample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDocumentTypes 8 | 9 | 10 | CFBundleTypeExtensions 11 | 12 | vgm 13 | 14 | CFBundleTypeName 15 | Video Game Music 16 | CFBundleTypeRole 17 | Viewer 18 | LSTypeIsPackage 19 | 0 20 | 21 | 22 | CFBundleTypeExtensions 23 | 24 | vgz 25 | 26 | CFBundleTypeName 27 | Video Game Music 28 | CFBundleTypeRole 29 | Viewer 30 | LSTypeIsPackage 31 | 0 32 | 33 | 34 | CFBundleExecutable 35 | $(EXECUTABLE_NAME) 36 | CFBundleIconFile 37 | 38 | CFBundleIdentifier 39 | $(PRODUCT_BUNDLE_IDENTIFIER) 40 | CFBundleInfoDictionaryVersion 41 | 6.0 42 | CFBundleName 43 | $(PRODUCT_NAME) 44 | CFBundlePackageType 45 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 46 | CFBundleShortVersionString 47 | $(MARKETING_VERSION) 48 | CFBundleVersion 49 | $(CURRENT_PROJECT_VERSION) 50 | LSApplicationCategoryType 51 | public.app-category.utilities 52 | LSMinimumSystemVersion 53 | $(MACOSX_DEPLOYMENT_TARGET) 54 | NSHumanReadableCopyright 55 | Copyright © 2020-2025 Kelvin Sherlock. All rights reserved. 56 | NSMainNibFile 57 | MainMenu 58 | NSPrincipalClass 59 | NSApplication 60 | NSSupportsAutomaticTermination 61 | 62 | NSSupportsSuddenTermination 63 | 64 | SUFeedURL 65 | https://ample.ksherlock.com/updates/appcast.xml 66 | SUPublicEDKey 67 | MgYKY5J1nIJ9+C3IabG24ri2M0CuoMqP78fva2GI5BY= 68 | 69 | 70 | -------------------------------------------------------------------------------- /Ample/LaunchWindowController.h: -------------------------------------------------------------------------------- 1 | // 2 | // LaunchWindowController.h 3 | // Ample 4 | // 5 | // Created by Kelvin Sherlock on 8/29/2020. 6 | // Copyright © 2020 Kelvin Sherlock. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface LaunchWindowController : NSWindowController 14 | 15 | @end 16 | 17 | NS_ASSUME_NONNULL_END 18 | -------------------------------------------------------------------------------- /Ample/LogWindowController.h: -------------------------------------------------------------------------------- 1 | // 2 | // LogWindowController.h 3 | // Ample 4 | // 5 | // Created by Kelvin Sherlock on 8/29/2020. 6 | // Copyright © 2020 Kelvin Sherlock. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface LogWindowController : NSWindowController 14 | 15 | #if 0 16 | +(id)controllerForTask: (NSTask *)task; 17 | +(id)controllerForTask: (NSTask *)task close: (BOOL)close; 18 | #endif 19 | 20 | +(id)controllerForArgs: (NSArray *)args; 21 | +(id)controllerForArgs: (NSArray *)args close: (BOOL)close; 22 | @end 23 | 24 | NS_ASSUME_NONNULL_END 25 | -------------------------------------------------------------------------------- /Ample/LogWindowController.m: -------------------------------------------------------------------------------- 1 | // 2 | // LogWindowController.m 3 | // Ample 4 | // 5 | // Created by Kelvin Sherlock on 8/29/2020. 6 | // Copyright © 2020 Kelvin Sherlock. All rights reserved. 7 | // 8 | 9 | #import "Ample.h" 10 | #import "LogWindowController.h" 11 | 12 | static NSMutableSet *LogWindows; 13 | 14 | @interface LogWindowController () 15 | @property (unsafe_unretained) IBOutlet NSTextView *textView; 16 | 17 | @end 18 | 19 | @implementation LogWindowController { 20 | NSTask *_task; 21 | NSFileHandle *_handle; 22 | NSFont *_font; 23 | 24 | BOOL _close; 25 | BOOL _eof; 26 | } 27 | 28 | +(void)initialize { 29 | LogWindows = [NSMutableSet set]; 30 | } 31 | 32 | -(NSString *)windowNibName { 33 | return @"LogWindow"; 34 | } 35 | 36 | 37 | +(id)controllerForTask: (NSTask *)task close: (BOOL)close { 38 | LogWindowController *controller = [[LogWindowController alloc] initWithWindowNibName: @"LogWindow"]; 39 | [controller runTask: task close: close]; 40 | return controller; 41 | } 42 | 43 | 44 | +(id)controllerForArgs: (NSArray *)args close: (BOOL)close { 45 | 46 | NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; 47 | 48 | NSURL *url = MameURL(); 49 | 50 | if (!url) { 51 | NSAlert *alert = [NSAlert new]; 52 | 53 | [alert setMessageText: @"Unable to find MAME executable path"]; 54 | [alert runModal]; 55 | return nil; 56 | } 57 | 58 | NSTask *task = [NSTask new]; 59 | 60 | if (@available(macOS 10.13, *)) { 61 | [task setExecutableURL: url]; 62 | [task setCurrentDirectoryURL: MameWorkingDirectory()]; 63 | } else { 64 | [task setLaunchPath: MamePath()]; 65 | [task setCurrentDirectoryPath: MameWorkingDirectoryPath()]; 66 | } 67 | 68 | [task setArguments: args]; 69 | 70 | if ([defaults boolForKey: kUseLogWindow] == NO) { 71 | 72 | NSAlert *alert = nil; 73 | if (@available(macOS 10.13, *)) { 74 | NSError *error = nil; 75 | 76 | [task launchAndReturnError: &error]; 77 | if (error) { 78 | alert = [NSAlert alertWithError: error]; 79 | } 80 | } else { 81 | @try { 82 | [task launch]; 83 | } @catch (NSException *exception) { 84 | 85 | alert = [NSAlert new]; 86 | [alert setMessageText: [exception reason]]; 87 | } 88 | } 89 | if (alert) [alert runModal]; 90 | return nil; 91 | } 92 | 93 | 94 | return [LogWindowController controllerForTask: task close: close]; 95 | 96 | } 97 | 98 | +(id)controllerForArgs: (NSArray *)args { 99 | 100 | NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; 101 | BOOL close = [defaults boolForKey: kAutoCloseLogWindow]; 102 | return [self controllerForArgs: args close: close]; 103 | } 104 | 105 | - (void)windowDidLoad { 106 | [super windowDidLoad]; 107 | 108 | [LogWindows addObject: self]; 109 | 110 | _font = [NSFont userFixedPitchFontOfSize: 0]; 111 | 112 | } 113 | 114 | -(void)appendString: (NSString *)string 115 | { 116 | if ([string length]) 117 | { 118 | // needs explicit color attribute for proper dark mode support. 119 | NSDictionary *attr = @{ 120 | NSForegroundColorAttributeName: [NSColor textColor], 121 | NSFontAttributeName: _font, 122 | }; 123 | NSAttributedString *astr = [[NSAttributedString alloc] initWithString: string attributes: attr]; 124 | [[_textView textStorage] appendAttributedString: astr]; 125 | } 126 | } 127 | 128 | -(void)appendAttributedString: (NSAttributedString *)string { 129 | 130 | if ([string length]) { 131 | [[_textView textStorage] appendAttributedString: string]; 132 | } 133 | } 134 | 135 | -(NSError *)runTask: (NSTask *)task close: (BOOL)close { 136 | 137 | 138 | if (_task) return nil; 139 | _close = close; 140 | _eof = NO; 141 | 142 | NSPipe *pipe = [NSPipe pipe]; 143 | 144 | // window not yet loaded until [self window] called. 145 | 146 | const char *path = nil; 147 | const char *wd = nil; 148 | 149 | 150 | [task setStandardError: pipe]; 151 | [task setStandardOutput: pipe]; 152 | if (@available(macOS 10.13, *)) { 153 | NSError *error = nil; 154 | path = [[task executableURL] fileSystemRepresentation]; 155 | wd = [[task currentDirectoryURL] fileSystemRepresentation]; 156 | 157 | [task launchAndReturnError: &error]; 158 | if (error) { 159 | NSLog(@"NSTask error. Path = %s error = %@", path, error); 160 | return error; 161 | } 162 | } else { 163 | path = [[task launchPath] fileSystemRepresentation]; 164 | wd = [[task currentDirectoryPath] fileSystemRepresentation]; 165 | @try { 166 | [task launch]; 167 | } @catch (NSException *exception) { 168 | 169 | NSLog(@"NSTask exception. Path = %s exception = %@", path, exception); 170 | return nil; // ? 171 | } 172 | } 173 | 174 | _task = task; 175 | NSString *title = [NSString stringWithFormat: @"Ample Log - %u", [task processIdentifier]]; 176 | [[self window] setTitle: title]; 177 | _handle = [pipe fileHandleForReading]; 178 | 179 | if (path) [self appendString: [NSString stringWithFormat: @"MAME path: %s\n", path]]; 180 | if (wd) [self appendString: [NSString stringWithFormat: @"Working Directory: %s\n", wd]]; 181 | 182 | NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; 183 | 184 | 185 | [nc addObserver: self 186 | selector: @selector(taskComplete:) 187 | name: NSTaskDidTerminateNotification 188 | object: _task]; 189 | [nc addObserver: self 190 | selector: @selector(readComplete:) 191 | name: NSFileHandleReadCompletionNotification 192 | object: _handle]; 193 | 194 | [_handle readInBackgroundAndNotify]; 195 | 196 | [[self window] setDocumentEdited: YES]; 197 | return nil; 198 | } 199 | 200 | 201 | #pragma mark - 202 | #pragma mark Notifications 203 | -(void)readComplete:(NSNotification *)notification 204 | { 205 | // read complete, queue up another. 206 | NSDictionary *dict = [notification userInfo]; 207 | NSData *data = [dict objectForKey: NSFileHandleNotificationDataItem]; 208 | 209 | if ([data length]) 210 | { 211 | NSString *string = [[NSString alloc] initWithData: data encoding: NSUTF8StringEncoding]; 212 | 213 | [self appendString: string]; 214 | 215 | [_handle readInBackgroundAndNotify]; 216 | } else { 217 | [self appendString: @"\n"]; // -listmedia sometimes causes display issues. 218 | _eof = YES; 219 | //[_textView setNeedsDisplay: YES]; // -listmedia sometimes weird. 220 | } 221 | } 222 | 223 | -(void)taskCompleteHack { 224 | 225 | } 226 | 227 | /* hask! task complete may occur while output still being processed. add a delay to compensate. */ 228 | -(void)taskComplete: (NSNotification *)notification 229 | { 230 | if (!_eof) { 231 | [self performSelector: @selector(taskComplete:) withObject: notification afterDelay: 0.5]; 232 | return; 233 | } 234 | 235 | BOOL ok = NO; 236 | NSTaskTerminationReason reason; 237 | int status; 238 | NSString *string = nil; 239 | 240 | reason = [_task terminationReason]; 241 | status = [_task terminationStatus]; 242 | 243 | if (reason == NSTaskTerminationReasonExit) 244 | { 245 | 246 | if (status == 0) 247 | { 248 | //string = @"\n\n[Success]\n\n"; 249 | ok = YES; 250 | } 251 | else string = @"\n\n[An error occurred]\n\n"; 252 | } 253 | else 254 | { 255 | string = [NSString stringWithFormat: @"\n\n[Caught signal %d (%s)]\n\n", status, strsignal(status)]; 256 | } 257 | if (string) { 258 | NSDictionary *attr = @{ 259 | NSForegroundColorAttributeName: [NSColor systemRedColor], 260 | NSFontAttributeName: _font, 261 | }; 262 | NSAttributedString *astr = [[NSAttributedString alloc] initWithString: string attributes: attr]; 263 | [self appendAttributedString: astr]; 264 | } 265 | 266 | _handle = nil; 267 | _task = nil; 268 | 269 | [[self window] setDocumentEdited: NO]; 270 | 271 | if (ok && _close) { 272 | [[self window] close]; 273 | } 274 | } 275 | 276 | #pragma mark - NSWindowDelegate 277 | 278 | 279 | -(void)windowWillClose:(NSNotification *)notification { 280 | [LogWindows removeObject: self]; 281 | } 282 | 283 | #pragma mark - IBActions 284 | 285 | - (IBAction)clearLog:(id)sender { 286 | NSAttributedString *empty = [NSAttributedString new]; 287 | [[_textView textStorage] setAttributedString: empty]; 288 | } 289 | 290 | 291 | @end 292 | -------------------------------------------------------------------------------- /Ample/MachineViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // MachineViewController.h 3 | // Ample 4 | // 5 | // Created by Kelvin Sherlock on 8/16/2020. 6 | // Copyright © 2020 Kelvin Sherlock. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | #import "Ample.h" 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | 15 | @interface MachineViewController : NSViewController 16 | 17 | @property NSString *machine; 18 | 19 | @end 20 | 21 | @interface MachineViewController (Bookmark) 22 | 23 | @end 24 | 25 | NS_ASSUME_NONNULL_END 26 | -------------------------------------------------------------------------------- /Ample/MachineViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // MachineViewController.m 3 | // Ample 4 | // 5 | // Created by Kelvin Sherlock on 8/16/2020. 6 | // Copyright © 2020 Kelvin Sherlock. All rights reserved. 7 | // 8 | 9 | #import "MachineViewController.h" 10 | 11 | @interface MachineViewController() 12 | 13 | @property NSArray *data; 14 | 15 | @end 16 | 17 | @implementation MachineViewController 18 | 19 | 20 | -(void)awakeFromNib { 21 | 22 | NSBundle *bundle = [NSBundle mainBundle]; 23 | #ifdef AMPLE_LITE 24 | NSString *path = [bundle pathForResource: @"models~extra" ofType: @"plist"]; 25 | #else 26 | NSString *path = [bundle pathForResource: @"models" ofType: @"plist"]; 27 | #endif 28 | _data = [NSArray arrayWithContentsOfFile: path]; 29 | 30 | 31 | /* XCode/Interface Builder 11.3 barfs on NSBrowser. */ 32 | 33 | NSBrowser *browser; 34 | NSRect frame = NSMakeRect(0, 0, 718, 200); 35 | 36 | browser = [[NSBrowser alloc] initWithFrame: frame]; 37 | 38 | [browser setMaxVisibleColumns: 2]; 39 | //[browser setTakesTitleFromPreviousColumn: YES]; 40 | //[browser setTitled: NO]; 41 | [browser setAllowsEmptySelection: NO]; 42 | [browser setDelegate: self]; 43 | [browser setAction: @selector(clickAction:)]; 44 | 45 | [self setView: browser]; 46 | } 47 | 48 | -(IBAction)clickAction:(id)sender { 49 | 50 | NSDictionary *item = [self itemForBrowser: sender]; 51 | [self setMachine: [item objectForKey: @"value"]]; 52 | } 53 | 54 | #pragma mark NSBrowser 55 | 56 | -(NSDictionary *)itemForBrowser: (NSBrowser *)browser { 57 | 58 | NSIndexPath *path = [browser selectionIndexPath]; 59 | 60 | NSArray *data = _data; 61 | NSDictionary *item = nil; 62 | 63 | NSUInteger l = [path length]; 64 | for (NSUInteger i = 0; i < l; ++i) { 65 | NSUInteger ix = [path indexAtPosition: i]; 66 | if (ix > [data count]) return nil; 67 | item = [data objectAtIndex: ix]; 68 | data = [item objectForKey: @"children"]; 69 | } 70 | 71 | return item; 72 | } 73 | -(NSArray *)itemsForBrowser: (NSBrowser *)browser column: (NSInteger) column { 74 | 75 | NSArray *data = _data; 76 | for (unsigned i = 0; i < column; ++i) { 77 | NSInteger ix = [browser selectedRowInColumn: i]; 78 | if (ix < 0) return 0; 79 | 80 | NSDictionary *item = [data objectAtIndex: ix]; 81 | data = [item objectForKey: @"children"]; 82 | if (!data) return 0; 83 | } 84 | return data; 85 | 86 | } 87 | 88 | - (void)browser:(NSBrowser *)sender willDisplayCell:(id)cell atRow:(NSInteger)row column:(NSInteger)column { 89 | NSArray *data = [self itemsForBrowser: sender column: column]; 90 | if (!data || row >= [data count]) return; 91 | 92 | NSDictionary *item = [data objectAtIndex: row]; 93 | 94 | NSBrowserCell *bc = (NSBrowserCell *)cell; 95 | 96 | [bc setStringValue: [item objectForKey: @"description"]]; 97 | [bc setLeaf: ![item objectForKey: @"children"]]; 98 | } 99 | 100 | 101 | - (NSString *)browser:(NSBrowser *)sender titleOfColumn:(NSInteger)column { 102 | return column == 0 ? @"Model" : @""; 103 | } 104 | 105 | #if 0 106 | - (id)browser:(NSBrowser *)browser child:(NSInteger)index ofItem:(id)item { 107 | return nil; 108 | } 109 | #endif 110 | 111 | - (NSInteger)browser:(NSBrowser *)sender numberOfRowsInColumn:(NSInteger)column { 112 | 113 | NSArray *data = [self itemsForBrowser: sender column: column]; 114 | return [data count]; 115 | } 116 | 117 | @end 118 | 119 | @implementation MachineViewController (Bookmark) 120 | 121 | -(BOOL)loadBookmark: (NSDictionary *)bookmark { 122 | 123 | NSBrowser *browser = (NSBrowser *)[self view]; 124 | NSString *machine = [bookmark objectForKey: @"machine"]; 125 | 126 | NSIndexPath *path = nil; 127 | NSUInteger ix[2] = {0, 0 }; 128 | for (NSDictionary *d in _data) { 129 | 130 | NSArray *children = [d objectForKey: @"children"]; 131 | 132 | for (NSDictionary *dd in children) { 133 | NSString *value = [dd objectForKey: @"value"]; 134 | 135 | if ([machine isEqualToString: value]) { 136 | path = [NSIndexPath indexPathWithIndexes: ix length: 2]; 137 | [browser selectRow: ix[0] inColumn: 0]; 138 | [browser selectRow: ix[1] inColumn: 1]; 139 | 140 | //[browser setSelectionIndexPath: path]; 141 | return YES; 142 | } 143 | ++ix[1]; 144 | } 145 | ix[1] = 0; 146 | 147 | 148 | // check parent after. 149 | NSString *value = [d objectForKey: @"value"]; 150 | if ([machine isEqualToString: value]) { 151 | path = [NSIndexPath indexPathWithIndexes: ix length: 1]; 152 | [browser selectRow: ix[0] inColumn: 0]; 153 | // "setSelectionIndexPath: is not supported for browsers with matrix delegates." 154 | //[browser setSelectionIndexPath: path]; 155 | return YES; 156 | } 157 | 158 | 159 | 160 | 161 | 162 | ++ix[0]; 163 | 164 | } 165 | NSLog(@"MachineViewController: Unable to find %@", machine); 166 | return NO; 167 | } 168 | 169 | -(BOOL)saveBookmark: (NSMutableDictionary *)bookmark { 170 | // machine saved in parent. 171 | return YES; 172 | } 173 | 174 | -(void)willLoadBookmark:(NSDictionary *)bookmark { 175 | } 176 | 177 | -(void)didLoadBookmark:(NSDictionary *)bookmark { 178 | } 179 | 180 | @end 181 | -------------------------------------------------------------------------------- /Ample/Media.h: -------------------------------------------------------------------------------- 1 | // 2 | // Media.h 3 | // Ample 4 | // 5 | // Created by Kelvin Sherlock on 3/7/2021. 6 | // Copyright © 2021 Kelvin Sherlock. All rights reserved. 7 | // 8 | 9 | #ifndef Media_h 10 | #define Media_h 11 | 12 | 13 | typedef struct Media { 14 | unsigned cass; 15 | unsigned cdrom; 16 | unsigned hard; 17 | unsigned floppy_8; 18 | unsigned floppy_5_25; 19 | unsigned floppy_3_5; 20 | unsigned pseudo_disk; 21 | unsigned bitbanger; 22 | unsigned midiin; 23 | unsigned midiout; 24 | unsigned picture; 25 | unsigned rom; 26 | uint64_t floppy_mask_8; 27 | uint64_t floppy_mask_5_25; 28 | uint64_t floppy_mask_3_5; 29 | } Media; 30 | 31 | 32 | typedef enum { 33 | MediaTypeError = -1, 34 | MediaTypeUnknown = 0, 35 | MediaType_8, 36 | MediaType_5_25, 37 | MediaType_3_5, 38 | MediaType_HardDisk, 39 | MediaType_CDROM, 40 | MediaType_Cassette, 41 | MediaType_Picture, 42 | MediaType_MIDI, 43 | MediaType_ROM, 44 | } MediaType; 45 | 46 | struct Media MediaFromDictionary(NSDictionary *); 47 | 48 | void MediaAdd(Media *dest, const Media *src); 49 | 50 | BOOL MediaEqual(const Media *lhs, const Media *rhs); 51 | 52 | extern const Media EmptyMedia; 53 | 54 | MediaType ClassifyMediaFile(id file); 55 | 56 | #endif /* Media_h */ 57 | -------------------------------------------------------------------------------- /Ample/MediaViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // MediaViewController.h 3 | // Ample 4 | // 5 | // Created by Kelvin Sherlock on 8/20/2020. 6 | // Copyright © 2020 Kelvin Sherlock. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "Media.h" 11 | #import "Ample.h" 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | 15 | @interface MediaViewController : NSViewController 16 | 17 | @property (weak) IBOutlet NSOutlineView *outlineView; 18 | @property (nonatomic) Media media; 19 | @property NSArray *args; 20 | 21 | - (IBAction)ejectAction:(id)sender; 22 | - (IBAction)pathAction:(id)sender; 23 | 24 | -(IBAction)resetMedia:(id)sender; 25 | 26 | -(BOOL)smartRouteURL: (NSURL *)url; 27 | -(BOOL)smartRouteFile: (NSString *)file; 28 | 29 | @end 30 | 31 | @interface MediaViewController (Bookmark) 32 | 33 | @end 34 | 35 | 36 | 37 | NS_ASSUME_NONNULL_END 38 | -------------------------------------------------------------------------------- /Ample/Menu.h: -------------------------------------------------------------------------------- 1 | // 2 | // Menu.h 3 | // Ample 4 | // 5 | // Created by Kelvin Sherlock on 10/3/2020. 6 | // Copyright © 2020 Kelvin Sherlock. All rights reserved. 7 | // 8 | 9 | #ifndef Menu_h 10 | #define Menu_h 11 | 12 | NSFont *ItalicMenuFont(void); 13 | NSAttributedString *ItalicMenuString(NSString *s); 14 | 15 | #endif /* Menu_h */ 16 | -------------------------------------------------------------------------------- /Ample/Menu.m: -------------------------------------------------------------------------------- 1 | // 2 | // Menu.m 3 | // Ample 4 | // 5 | // Created by Kelvin Sherlock on 10/3/2020. 6 | // Copyright © 2020 Kelvin Sherlock. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | 12 | NSFont *ItalicMenuFont(void) { 13 | NSFont *font = [NSFont menuFontOfSize: 0]; 14 | NSFontDescriptor *fd = [font fontDescriptor]; 15 | NSFontDescriptor *fd2 = [fd fontDescriptorWithSymbolicTraits: NSFontDescriptorTraitItalic]; 16 | return [NSFont fontWithDescriptor: fd2 size: [font pointSize]]; 17 | } 18 | 19 | NSAttributedString *ItalicMenuString(NSString *s) { 20 | static NSDictionary *attr = nil; 21 | if (!attr) { 22 | attr = @{ 23 | NSFontAttributeName: ItalicMenuFont() 24 | }; 25 | } 26 | return [[NSAttributedString alloc] initWithString: s attributes: attr]; 27 | } 28 | -------------------------------------------------------------------------------- /Ample/MidiManager.h: -------------------------------------------------------------------------------- 1 | // 2 | // MidiManager.h 3 | // Ample 4 | // 5 | // Created by Kelvin Sherlock on 8/6/2021. 6 | // Copyright © 2021 Kelvin Sherlock. All rights reserved. 7 | // 8 | 9 | #ifndef MidiManager_h 10 | #define MidiManager_h 11 | 12 | 13 | extern NSString *kMidiSourcesChangedNotification; 14 | extern NSString *kMidiDestinationsChangedNotification; 15 | 16 | @interface MidiManager : NSObject 17 | 18 | @property NSArray *sources; 19 | @property NSArray *destinations; 20 | 21 | +(instancetype)sharedManager; 22 | 23 | @end 24 | 25 | #endif /* MidiManager_h */ 26 | -------------------------------------------------------------------------------- /Ample/MidiManager.m: -------------------------------------------------------------------------------- 1 | // 2 | // Midi.m 3 | // Ample 4 | // 5 | // Created by Kelvin Sherlock on 8/6/2021. 6 | // Copyright © 2021 Kelvin Sherlock. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | 12 | #import "MidiManager.h" 13 | 14 | static NSArray *MidiSources(void) { 15 | 16 | ItemCount count = MIDIGetNumberOfSources(); 17 | if (count <= 0) return @[]; 18 | 19 | NSMutableArray *rv = [NSMutableArray arrayWithCapacity: count + 1]; 20 | 21 | MIDIEndpointRef ep; 22 | for(int i = 0; i < count; ++i) { 23 | ep = MIDIGetSource(i); 24 | if (!ep) continue; 25 | 26 | // https://developer.apple.com/library/archive/qa/qa1374/_index.html 27 | CFStringRef str = NULL; 28 | MIDIObjectGetStringProperty(ep, kMIDIPropertyDisplayName, &str); 29 | 30 | if (str) { 31 | [rv addObject: (__bridge id _Nonnull)(str)]; 32 | CFRelease(str); 33 | } 34 | } 35 | return rv; 36 | } 37 | 38 | 39 | static NSArray *MidiDestinations(void) { 40 | 41 | ItemCount count = MIDIGetNumberOfDestinations(); 42 | if (count <= 0) return @[]; 43 | 44 | NSMutableArray *rv = [NSMutableArray arrayWithCapacity: count + 1]; 45 | 46 | MIDIEndpointRef ep; 47 | for(int i = 0; i < count; ++i) { 48 | ep = MIDIGetDestination(i); 49 | if (!ep) continue; 50 | 51 | // https://developer.apple.com/library/archive/qa/qa1374/_index.html 52 | CFStringRef str = NULL; 53 | MIDIObjectGetStringProperty(ep, kMIDIPropertyDisplayName, &str); 54 | 55 | if (str) { 56 | [rv addObject: (__bridge id _Nonnull)(str)]; 57 | CFRelease(str); 58 | } 59 | } 60 | return rv; 61 | } 62 | 63 | NSString *kMidiSourcesChangedNotification = @"Midi Sources Changed"; 64 | NSString *kMidiDestinationsChangedNotification = @"Midi Destinations Changed"; 65 | 66 | 67 | @interface MidiManager () { 68 | MIDIClientRef _client; 69 | } 70 | 71 | -(void)objectAddRemove: (const MIDIObjectAddRemoveNotification *)message; 72 | -(void)objectPropertyChanged: (const MIDIObjectPropertyChangeNotification *)message; 73 | @end 74 | 75 | 76 | static MidiManager *singleton = nil; 77 | @implementation MidiManager 78 | 79 | -(void)awakeFromNib { 80 | if (!singleton) singleton = self; 81 | } 82 | 83 | +(instancetype)sharedManager { 84 | if (!singleton) singleton = [MidiManager new]; 85 | return singleton; 86 | } 87 | 88 | -(instancetype)init { 89 | 90 | if (singleton) return singleton; 91 | 92 | OSStatus status; 93 | 94 | 95 | status = MIDIClientCreateWithBlock( 96 | CFSTR("serial_midi"), 97 | &_client, 98 | ^(const MIDINotification *message){ 99 | switch(message->messageID) { 100 | case kMIDIMsgObjectAdded: 101 | case kMIDIMsgObjectRemoved: 102 | [self objectAddRemove: (const MIDIObjectAddRemoveNotification *)message]; 103 | break; 104 | case kMIDIMsgPropertyChanged: 105 | [self objectPropertyChanged: (const MIDIObjectPropertyChangeNotification *)message]; 106 | default: 107 | break; 108 | } 109 | }); 110 | 111 | _sources = MidiSources(); 112 | _destinations = MidiDestinations(); 113 | return self; 114 | } 115 | 116 | -(void)objectAddRemove: (const MIDIObjectAddRemoveNotification *)message { 117 | 118 | const MIDIObjectAddRemoveNotification *m = (const MIDIObjectAddRemoveNotification *)message; 119 | 120 | NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; 121 | 122 | if (m->childType == kMIDIObjectType_Source) { 123 | [self setSources: MidiSources()]; 124 | [nc postNotificationName: kMidiSourcesChangedNotification object: self]; 125 | } 126 | 127 | if (m->childType == kMIDIObjectType_Destination) { 128 | [self setDestinations: MidiDestinations()]; 129 | [nc postNotificationName: kMidiDestinationsChangedNotification object: self]; 130 | } 131 | 132 | } 133 | -(void)objectPropertyChanged: (const MIDIObjectPropertyChangeNotification *)message { 134 | 135 | const MIDIObjectPropertyChangeNotification *m = (const MIDIObjectPropertyChangeNotification *)message; 136 | if (m->propertyName == kMIDIPropertyDisplayName) { 137 | [self setSources: MidiSources()]; 138 | [self setDestinations: MidiDestinations()]; 139 | 140 | NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; 141 | [nc postNotificationName: kMidiSourcesChangedNotification object: self]; 142 | [nc postNotificationName: kMidiDestinationsChangedNotification object: self]; 143 | } 144 | } 145 | 146 | 147 | -(void)dealloc { 148 | 149 | if (_client) 150 | MIDIClientDispose(_client); 151 | } 152 | @end 153 | 154 | -------------------------------------------------------------------------------- /Ample/NewMachineViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // NewMachineViewController.h 3 | // Ample 4 | // 5 | // Created by Kelvin Sherlock on 6/8/2021. 6 | // Copyright © 2021 Kelvin Sherlock. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "Ample.h" 11 | 12 | NS_ASSUME_NONNULL_BEGIN 13 | 14 | @interface NewMachineViewController : NSViewController 15 | 16 | @property (nullable) NSString *machine; 17 | 18 | -(void)reset; 19 | 20 | @end 21 | 22 | @interface NewMachineViewController (Table) 23 | 24 | @end 25 | 26 | @interface NewMachineViewController (Bookmark) 27 | 28 | @end 29 | 30 | NS_ASSUME_NONNULL_END 31 | -------------------------------------------------------------------------------- /Ample/NewMachineViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // NewMachineViewController.m 3 | // Ample 4 | // 5 | // Created by Kelvin Sherlock on 6/8/2021. 6 | // Copyright © 2021 Kelvin Sherlock. All rights reserved. 7 | // 8 | 9 | #import "NewMachineViewController.h" 10 | 11 | @interface NewMachineViewController () { 12 | 13 | NSArray *_data; 14 | } 15 | 16 | @property (weak) IBOutlet NSOutlineView *outlineView; 17 | 18 | @end 19 | 20 | @implementation NewMachineViewController 21 | 22 | -(void)awakeFromNib { 23 | 24 | static unsigned first = 0; 25 | 26 | if (first) return; 27 | first++; 28 | 29 | NSBundle *bundle = [NSBundle mainBundle]; 30 | #ifdef AMPLE_LITE 31 | NSString *path = [bundle pathForResource: @"models~extra" ofType: @"plist"]; 32 | #else 33 | NSString *path = [bundle pathForResource: @"models" ofType: @"plist"]; 34 | #endif 35 | _data = [NSArray arrayWithContentsOfFile: path]; 36 | 37 | } 38 | 39 | -(void)viewDidLoad { 40 | [super viewDidLoad]; 41 | 42 | //[_outlineView reloadData]; 43 | //[_outlineView setAutosaveExpandedItems: YES]; 44 | //[_outlineView expandItem: nil expandChildren: YES]; 45 | } 46 | 47 | #pragma mark - IBActions 48 | - (IBAction)clickAction:(id)sender { 49 | 50 | NSInteger row = [_outlineView clickedRow]; 51 | if (row < 0) return; 52 | NSDictionary *item = [_outlineView itemAtRow: row]; 53 | if (!item) return; 54 | 55 | NSString *value = [item objectForKey: @"value"]; 56 | NSArray *children = [item objectForKey: @"children"]; 57 | 58 | if (value) { 59 | [self setMachine: value]; 60 | } else if (children) { 61 | id ap = [_outlineView animator]; 62 | [_outlineView isItemExpanded: item] ? [ap collapseItem: item] : [ap expandItem: item]; 63 | } 64 | 65 | 66 | } 67 | 68 | -(void)reset { 69 | 70 | [_outlineView deselectAll: nil]; 71 | [self setMachine: nil]; 72 | } 73 | @end 74 | 75 | 76 | @implementation NewMachineViewController (Table) 77 | 78 | #if 0 79 | - (BOOL)outlineView:(NSOutlineView *)outlineView shouldExpandItem:(id)item { 80 | return YES; 81 | } 82 | 83 | - (BOOL)outlineView:(NSOutlineView *)outlineView shouldCollapseItem:(id)item { 84 | return NO; 85 | } 86 | #endif 87 | 88 | - (BOOL)outlineView:(NSOutlineView *)outlineView shouldShowOutlineCellForItem:(id)item { 89 | // disclosure triangle. 90 | if (!item) return YES; 91 | NSArray *children = [(NSDictionary *)item objectForKey: @"children"]; 92 | return [children count] > 0; 93 | } 94 | 95 | 96 | - (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item { 97 | if (!item) return YES; 98 | NSArray *children = [(NSDictionary *)item objectForKey: @"children"]; 99 | return [children count] > 0; 100 | } 101 | 102 | - (BOOL)outlineView:(NSOutlineView *)outlineView isGroupItem:(id)item { 103 | 104 | #if 0 105 | NSArray *children = [(NSDictionary *)item objectForKey: @"children"]; 106 | return [children count] > 0; 107 | #else 108 | return NO; 109 | #endif 110 | } 111 | 112 | 113 | - (NSView *)outlineView:(NSOutlineView *)outlineView viewForTableColumn:(NSTableColumn *)tableColumn item:(id)item { 114 | 115 | #if 0 116 | NSArray *children = [(NSDictionary *)item objectForKey: @"children"]; 117 | if ([children count]) { 118 | return [outlineView makeViewWithIdentifier: @"HeaderCell" owner: self]; 119 | } 120 | #endif 121 | NSTableCellView *v = [outlineView makeViewWithIdentifier: @"DataCell" owner: self]; 122 | //[v setObjectValue: item]; 123 | return v; 124 | } 125 | 126 | - (NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item { 127 | if (!item) return [_data count]; 128 | NSArray *children = [(NSDictionary *)item objectForKey: @"children"]; 129 | return [children count]; 130 | } 131 | 132 | - (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item { 133 | 134 | if (item == nil) { 135 | return [_data objectAtIndex: index]; 136 | } 137 | NSArray *children = [(NSDictionary *)item objectForKey: @"children"]; 138 | return [children objectAtIndex: index]; 139 | } 140 | 141 | - (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item { 142 | return item; 143 | } 144 | 145 | 146 | - (BOOL)outlineView:(NSOutlineView *)outlineView shouldSelectItem:(id)item { 147 | if (!item) return NO; 148 | return [(NSDictionary *)item objectForKey: @"value"] != nil; 149 | } 150 | 151 | 152 | // saving/restoring expanded items 153 | - (id)outlineView:(NSOutlineView *)outlineView persistentObjectForItem:(id)item { 154 | 155 | return [item objectForKey: @"description"]; 156 | } 157 | 158 | - (id)outlineView:(NSOutlineView *)outlineView itemForPersistentObject:(id)object { 159 | 160 | if ([object isKindOfClass: [NSString class]]) { 161 | 162 | for(NSDictionary *d in _data) { 163 | if ([(NSString *)object isEqualToString: [d objectForKey: @"description"]]) 164 | return d; 165 | } 166 | 167 | } 168 | return nil; 169 | //return object; 170 | } 171 | 172 | 173 | @end 174 | 175 | 176 | @implementation NewMachineViewController (Bookmark) 177 | 178 | 179 | - (void)didLoadBookmark:(NSDictionary *)bookmark { 180 | } 181 | 182 | - (BOOL)loadBookmark:(NSDictionary *)bookmark { 183 | NSString *machine = [bookmark objectForKey: @"machine"]; 184 | 185 | 186 | 187 | //NSInteger row = [_outlineView selectedRow]; 188 | if (!machine) { 189 | [self setMachine: nil]; 190 | [_outlineView deselectAll: nil]; 191 | return NO; 192 | } 193 | 194 | for (NSDictionary *parent in _data) { 195 | NSArray *children = [parent objectForKey: @"children"]; 196 | 197 | for (NSDictionary *child in children) { 198 | if ([machine isEqualToString: [child objectForKey: @"value"]]) { 199 | 200 | id ap = [_outlineView animator]; 201 | [ap expandItem: parent]; 202 | NSInteger row = [_outlineView rowForItem: child]; 203 | if (row >= 0) { 204 | NSIndexSet *set = [NSIndexSet indexSetWithIndex: row]; 205 | [_outlineView selectRowIndexes: set byExtendingSelection: NO]; 206 | [_outlineView scrollRowToVisible: row]; 207 | return YES; 208 | } 209 | return NO; 210 | } 211 | } 212 | 213 | // could also match parent. 214 | if ([machine isEqualToString: [parent objectForKey: @"value"]]) { 215 | NSInteger row = [_outlineView rowForItem: parent]; 216 | if (row >= 0) { 217 | NSIndexSet *set = [NSIndexSet indexSetWithIndex: row]; 218 | [_outlineView selectRowIndexes: set byExtendingSelection: NO]; 219 | [_outlineView scrollRowToVisible: row]; 220 | return YES; 221 | } 222 | return NO; 223 | } 224 | } 225 | 226 | return NO; 227 | } 228 | 229 | - (BOOL)saveBookmark:(NSMutableDictionary *)bookmark { 230 | // machine saved in parent. 231 | return YES; 232 | } 233 | 234 | - (void)willLoadBookmark:(NSDictionary *)bookmark { 235 | 236 | } 237 | 238 | @end 239 | -------------------------------------------------------------------------------- /Ample/PreferencesWindowController.h: -------------------------------------------------------------------------------- 1 | // 2 | // PreferencesWindowController.h 3 | // Ample 4 | // 5 | // Created by Kelvin Sherlock on 8/31/2020. 6 | // Copyright © 2020 Kelvin Sherlock. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface PreferencesWindowController : NSWindowController 14 | 15 | @end 16 | 17 | NS_ASSUME_NONNULL_END 18 | -------------------------------------------------------------------------------- /Ample/PreferencesWindowController.m: -------------------------------------------------------------------------------- 1 | // 2 | // PreferencesWindowController.m 3 | // Ample 4 | // 5 | // Created by Kelvin Sherlock on 8/31/2020. 6 | // Copyright © 2020 Kelvin Sherlock. All rights reserved. 7 | // 8 | 9 | #import "Ample.h" 10 | #import "PreferencesWindowController.h" 11 | 12 | #import 13 | 14 | 15 | @interface PreferencesWindowController () 16 | @property (weak) IBOutlet NSTextField *pathField; 17 | @property (weak) IBOutlet NSTextField *wdField; 18 | @property (weak) IBOutlet NSButton *fixButton; 19 | 20 | @end 21 | 22 | @implementation PreferencesWindowController 23 | 24 | -(NSString *)windowNibName { 25 | return @"Preferences"; 26 | } 27 | 28 | - (void)windowDidLoad { 29 | NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; 30 | [super windowDidLoad]; 31 | 32 | // Implement this method to handle any initialization after your window controller's window has been loaded from its nib file. 33 | 34 | [self validateMamePath: [defaults stringForKey: kMamePath]]; 35 | [self validateMameWD: [defaults stringForKey: kMameWorkingDirectory]]; 36 | 37 | /* check vmnet_helper permissions */ 38 | 39 | int needs_fixin = [self checkHelperPermissions: nil]; 40 | [_fixButton setEnabled: needs_fixin > 0]; 41 | } 42 | 43 | -(void)validateMamePath: (NSString *)path { 44 | NSFileManager * fm = [NSFileManager defaultManager]; 45 | 46 | if ([path length] == 0 || [fm isExecutableFileAtPath: path]) { 47 | [_pathField setTextColor: nil]; 48 | } else { 49 | [_pathField setTextColor: [NSColor systemRedColor]]; 50 | } 51 | } 52 | 53 | -(void)validateMameWD: (NSString *)path { 54 | NSFileManager * fm = [NSFileManager defaultManager]; 55 | BOOL directory = YES; 56 | 57 | if ([path length] == 0) { 58 | [_wdField setTextColor: nil]; 59 | return; 60 | } 61 | 62 | if ([fm fileExistsAtPath: path isDirectory: &directory] && directory) { 63 | [_wdField setTextColor: nil]; 64 | return; 65 | 66 | } 67 | [_wdField setTextColor: [NSColor systemRedColor]]; 68 | } 69 | 70 | - (IBAction)pathChanged:(id)sender { 71 | 72 | NSString *path = [sender stringValue]; 73 | 74 | [self validateMamePath: path]; 75 | 76 | } 77 | - (IBAction)wdChanged:(id)sender { 78 | 79 | NSString *path = [sender stringValue]; 80 | 81 | [self validateMameWD: path]; 82 | } 83 | 84 | // -1 - error 85 | // 1 - needs help 86 | // 0 - a-ok 87 | -(int)checkHelperPermissions: (NSString *)path { 88 | 89 | static const unsigned Mask = S_ISUID | S_ISGID; 90 | if (!path) { 91 | NSBundle *bundle = [NSBundle mainBundle]; 92 | path = [bundle pathForAuxiliaryExecutable: @"vmnet_helper"]; 93 | } 94 | if (!path) return -1; 95 | 96 | NSFileManager *fm = [NSFileManager defaultManager]; 97 | NSError *error = nil; 98 | NSDictionary *attr = [fm attributesOfItemAtPath: path error: &error]; 99 | 100 | if (error) return -1; 101 | 102 | NSNumber *owner = [attr objectForKey: NSFileOwnerAccountID]; 103 | NSNumber *perm = [attr objectForKey: NSFilePosixPermissions]; 104 | if ([owner longValue] == 0 && ([perm unsignedIntValue] & Mask) == Mask) return 0; 105 | return 1; 106 | } 107 | 108 | - (IBAction)fixPerms:(id)sender { 109 | 110 | NSBundle *bundle = [NSBundle mainBundle]; 111 | NSString *path = [bundle pathForAuxiliaryExecutable: @"vmnet_helper"]; 112 | if (!path) return; 113 | 114 | 115 | #if 0 116 | // this requires an entitlement and sanboxing and Apple's permission. 117 | NSWorkspace *ws = [NSWorkspace sharedWorkspace]; 118 | 119 | [ws requestAuthorizationOfType:NSWorkspaceAuthorizationTypeSetAttributes 120 | completionHandler: ^(NSWorkspaceAuthorization *a, NSError *e){ 121 | if (e || !a) return; 122 | 123 | NSError *error = nil; 124 | NSDictionary *attr = @{ 125 | NSFileOwnerAccountID: @0, /* root */ 126 | NSFileGroupOwnerAccountID: @20, /* staff */ 127 | // NSFilePosixPermissions: @0106755 /* 755 + setuid + setgid */ 128 | }; 129 | 130 | 131 | 132 | NSFileManager *fm = [NSFileManager fileManagerWithAuthorization: a]; 133 | [fm setAttributes: attr ofItemAtPath: path error: &error]; 134 | if (error) { 135 | NSLog(@"%@", error); 136 | // NSAlert *a = [NSAlert alertWithError: error]; 137 | // [a runModal]; 138 | } 139 | else { 140 | [self->_fixButton setEnabled: NO]; 141 | } 142 | 143 | }]; 144 | #endif 145 | 146 | // AuthorizationExecuteWithPrivileges - deprecated in 10.7 147 | // https://github.com/sveinbjornt/STPrivilegedTask 148 | // XMJobBless + launchd stuff - the preferred way to do it... 149 | // https://developer.apple.com/library/archive/samplecode/BetterAuthorizationSample/Introduction/Intro.html 150 | // https://developer.apple.com/library/archive/samplecode/SMJobBless/Listings/ReadMe_txt.html#//apple_ref/doc/uid/DTS40010071-ReadMe_txt-DontLinkElementID_3 151 | // 152 | // really should be a launchd service but that's for another time... 153 | 154 | AuthorizationRef myAuthorizationRef = 0; 155 | OSStatus myStatus = AuthorizationCreate(NULL, kAuthorizationEmptyEnvironment, kAuthorizationFlagDefaults, &myAuthorizationRef); 156 | if (myStatus) return; 157 | 158 | AuthorizationItem myItems[1] = {{0}}; 159 | myItems[0].name = kAuthorizationRightExecute; 160 | myItems[0].valueLength = 0; 161 | myItems[0].value = NULL; 162 | myItems[0].flags = 0; 163 | AuthorizationRights myRights = {0}; 164 | myRights.count = sizeof(myItems) / sizeof(myItems[0]); 165 | myRights.items = myItems; 166 | AuthorizationFlags myFlags = kAuthorizationFlagDefaults | kAuthorizationFlagInteractionAllowed | 167 | kAuthorizationFlagExtendRights | kAuthorizationFlagPreAuthorize; 168 | myStatus = AuthorizationCopyRights(myAuthorizationRef, &myRights, 169 | kAuthorizationEmptyEnvironment, myFlags, NULL); 170 | 171 | if (!myStatus) { 172 | FILE *fp = NULL; 173 | static char buffer[4096]; 174 | const char *cp = [path fileSystemRepresentation]; 175 | const char* args_chown[] = {"root", cp , NULL}; 176 | const char* args_chmod[] = {"+s", cp, NULL}; 177 | 178 | // well ... the second command executes a lot more consistently when the (optional) fp is provided and the we fgets the buffer. 179 | // textmate does a wait() to wait for a child to exit. Using the fp (which is popened()?) and reading until EOF 180 | // accomplishes the same thing. 181 | myStatus = AuthorizationExecuteWithPrivileges(myAuthorizationRef, "/usr/sbin/chown", kAuthorizationFlagDefaults, (char**)args_chown, &fp); 182 | while (fgets(buffer, sizeof(buffer), fp)); 183 | fclose(fp); 184 | // fprintf(stderr, "myStatus = %d\ndata: %s\n", myStatus, buffer); 185 | 186 | myStatus = AuthorizationExecuteWithPrivileges(myAuthorizationRef, "/bin/chmod", kAuthorizationFlagDefaults, (char**)args_chmod, &fp); 187 | while (fgets(buffer, sizeof(buffer), fp)); 188 | fclose(fp); 189 | // fprintf(stderr, "myStatus = %d\ndata: %s\n", myStatus, buffer); 190 | 191 | } 192 | AuthorizationFree(myAuthorizationRef, kAuthorizationFlagDestroyRights); 193 | 194 | int needs_fixin = [self checkHelperPermissions: path]; 195 | [_fixButton setEnabled: needs_fixin > 0]; 196 | } 197 | 198 | 199 | @end 200 | -------------------------------------------------------------------------------- /Ample/Resources/CheatSheet.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Keyboard Cheat Sheet 6 | 58 | 83 | 84 | 85 |
86 | MacBook: 87 |
88 |

Full UI Mode

89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 111 | 112 | 113 | 114 |
97 | Delete 98 | Fn+Delete 99 | Toggle partial UI mode
Option+EnterToggle full screen mode
108 | F12 109 | Fn+F12 110 | Reset Key
115 | 116 |

Partial UI Mode

117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 |
126 | Delete 127 | Fn+Delete 128 | Toggle full UI mode
Option+EnterToggle full screen mode
TabConfig menu
EscQuit
F5Pause
~Debugger break (when active)
155 | Page Down 156 | Fn+Down Arrow 157 | Fast forward (while pressed)
F3Soft reset
Shift+F3Hard reset
Shift+F6Create save state
F7Load save state
F10Toggle throttle
F11Show FPS
Shift+F11Show profiler
F12Save snapshot
Shift+F12Record MNG video
Control+Shift+F12Record AVI video
Option+Shift+F12Record rendered AVI video
224 | 225 |

Note

226 |
    227 |
  • F keys may require Fn key depending on keyboard and system settings.
  • 228 |
  • Shift, Control, and Option assume left-key unless otherwise specified.
  • 229 |
230 | 231 | 232 | -------------------------------------------------------------------------------- /Ample/Resources/cz101.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | value 6 | cz101 7 | description 8 | CZ-101 9 | media 10 | 11 | 12 | resolution 13 | 14 | 97 15 | 38 16 | 17 | slots 18 | 19 | 20 | name 21 | ramsize 22 | description 23 | RAM 24 | options 25 | 26 | 27 | 28 | 29 | name 30 | bios 31 | description 32 | ROM 33 | options 34 | 35 | 36 | value 37 | 38 | description 39 | —Default— 40 | default 41 | 42 | 43 | 44 | value 45 | v2 46 | description 47 | Version II 48 | 49 | 50 | value 51 | v1 52 | description 53 | Version I 54 | 55 | 56 | 57 | 58 | name 59 | mdin 60 | description 61 | MIDI In 62 | options 63 | 64 | 65 | value 66 | 67 | description 68 | —None— 69 | default 70 | 71 | 72 | 73 | value 74 | midiin 75 | description 76 | MIDI In port 77 | default 78 | 79 | devname 80 | midiin_port 81 | media 82 | 83 | midiin 84 | 1 85 | 86 | 87 | 88 | 89 | 90 | name 91 | mdout 92 | description 93 | MIDI Out 94 | options 95 | 96 | 97 | value 98 | 99 | description 100 | —None— 101 | default 102 | 103 | 104 | 105 | value 106 | midiout 107 | description 108 | MIDI Out port 109 | default 110 | 111 | devname 112 | midiout_port 113 | media 114 | 115 | midiout 116 | 1 117 | 118 | 119 | 120 | 121 | 122 | devices 123 | 124 | 125 | software 126 | 127 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /Ample/Resources/las3000.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | value 6 | las3000 7 | description 8 | Laser 3000 9 | media 10 | 11 | cass 12 | 1 13 | 14 | resolution 15 | 16 | 560 17 | 192 18 | 19 | slots 20 | 21 | 22 | name 23 | ramsize 24 | description 25 | RAM 26 | options 27 | 28 | 29 | intValue 30 | 192 31 | description 32 | 192K 33 | value 34 | 192K 35 | default 36 | 37 | 38 | 39 | 40 | 41 | devices 42 | 43 | 44 | software 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /Ample/Resources/trs80.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | value 6 | trs80 7 | description 8 | TRS-80 Model I (Level I Basic) 9 | media 10 | 11 | 12 | resolution 13 | 14 | 384 15 | 192 16 | 17 | slots 18 | 19 | 20 | devices 21 | 22 | 23 | software 24 | 25 | 26 | name 27 | trs80_cass.xml 28 | filter 29 | 0 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /Ample/Slot.h: -------------------------------------------------------------------------------- 1 | // 2 | // Slot.h 3 | // Ample 4 | // 5 | // Created by Kelvin Sherlock on 3/6/2021. 6 | // Copyright © 2021 Kelvin Sherlock. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | #import "Media.h" 12 | 13 | 14 | //NS_ASSUME_NONNULL_BEGIN 15 | @class Slot, SlotOption, SlotTableCellView; 16 | 17 | 18 | typedef enum SlotType { 19 | kSlotRAM = 1, 20 | kSlotBIOS, 21 | kSlotFDC, 22 | } SlotType; 23 | 24 | @interface Slot : NSObject 25 | 26 | @property NSInteger defaultIndex; 27 | @property NSInteger selectedIndex; 28 | @property NSInteger index; 29 | 30 | @property (readonly) NSString *name; 31 | @property (readonly) NSString *title; 32 | @property (readonly) NSArray *menuItems; 33 | @property (readonly) SlotType type; 34 | 35 | @property (readonly) SlotOption *selectedItem; 36 | @property (readonly) NSString *selectedValue; 37 | 38 | -(NSArray *)args; 39 | -(NSDictionary *)serialize; 40 | -(void)reserialize: (NSDictionary *)dict; 41 | 42 | -(void)reset; 43 | -(void)prepareView: (SlotTableCellView *)view; 44 | 45 | -(BOOL)selectValue: (NSString *)value; 46 | 47 | -(Media)selectedMedia; 48 | 49 | -(NSArray *)selectedChildren; 50 | 51 | @end 52 | 53 | @interface SlotOption : NSObject 54 | 55 | @property NSString *value; 56 | @property NSString *title; 57 | @property BOOL isDefault; 58 | @property BOOL disabled; 59 | 60 | @end 61 | 62 | @interface SlotTableCellView : NSTableCellView 63 | 64 | @property (weak) IBOutlet NSPopUpButton *menuButton; 65 | @property (weak) IBOutlet NSButton *hamburgerButton; 66 | @end 67 | 68 | 69 | //NS_ASSUME_NONNULL_END 70 | -------------------------------------------------------------------------------- /Ample/SlotViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // SlotViewController.h 3 | // Ample 4 | // 5 | // Created by Kelvin Sherlock on 9/9/2020. 6 | // Copyright © 2020 Kelvin Sherlock. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "Media.h" 11 | #import "Ample.h" 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | 15 | @interface SlotViewController : NSViewController 16 | 17 | @property NSArray *args; 18 | @property Media media; 19 | @property NSSize resolution; 20 | @property (nonatomic, nullable) NSString *machine; 21 | 22 | -(IBAction)resetSlots:(nullable id)sender; 23 | 24 | @end 25 | 26 | @interface SlotViewController (OutlineView) 27 | 28 | @end 29 | 30 | @interface SlotViewController (Bookmark) 31 | 32 | @end 33 | 34 | 35 | NS_ASSUME_NONNULL_END 36 | -------------------------------------------------------------------------------- /Ample/SlotViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // SlotViewController.m 3 | // Ample 4 | // 5 | // Created by Kelvin Sherlock on 9/9/2020. 6 | // Copyright © 2020 Kelvin Sherlock. All rights reserved. 7 | // 8 | 9 | 10 | #import "Ample.h" 11 | #import "SlotViewController.h" 12 | #import "Menu.h" 13 | #import "Slot.h" 14 | #import "Media.h" 15 | 16 | 17 | #import 18 | 19 | #define MAX_SLOTS 32 20 | 21 | 22 | static unsigned RootKey = 0; 23 | 24 | 25 | @interface SlotViewController () 26 | @property (weak) IBOutlet NSOutlineView *outlineView; 27 | @property (weak) IBOutlet NSOutlineView *childOutlineView; 28 | 29 | @end 30 | 31 | @implementation SlotViewController { 32 | NSArray *_root; 33 | 34 | Media _slot_media[MAX_SLOTS]; 35 | Media _machine_media; 36 | 37 | NSDictionary *_machine_data; 38 | 39 | NSMutableDictionary *_slotValues; 40 | 41 | IBOutlet NSPopover *_popover; 42 | 43 | BOOL _loadingBookmark; 44 | } 45 | 46 | - (void)viewDidLoad { 47 | [super viewDidLoad]; 48 | // Do view setup here. 49 | 50 | _root = @[]; 51 | objc_setAssociatedObject(_outlineView, &RootKey, _root, OBJC_ASSOCIATION_RETAIN); 52 | 53 | _slotValues = [NSMutableDictionary new]; 54 | 55 | //[_outlineView setIndentationPerLevel: 2.0]; 56 | } 57 | 58 | -(void)resetMachine { 59 | 60 | _root = @[]; 61 | objc_setAssociatedObject(_outlineView, &RootKey, _root, OBJC_ASSOCIATION_RETAIN); 62 | 63 | [_outlineView reloadData]; 64 | 65 | [_slotValues removeAllObjects]; 66 | 67 | _machine_media = EmptyMedia; 68 | _machine_data = nil; 69 | 70 | for (unsigned i = 0; i < MAX_SLOTS; ++i) { 71 | _slot_media[i] = EmptyMedia; 72 | } 73 | 74 | [self setResolution: NSMakeSize(0, 0)]; 75 | [self setArgs: @[]]; 76 | [self setMedia: EmptyMedia]; 77 | } 78 | 79 | 80 | -(void)loadMachine { 81 | 82 | NSDictionary *d = MameMachine(_machine); 83 | 84 | if (!d) { 85 | [self resetMachine]; 86 | return; 87 | } 88 | 89 | NSArray *r = [d objectForKey: @"resolution"]; 90 | NSSize res = NSMakeSize(0, 0); 91 | if (r) { 92 | res.width = [(NSNumber *)[r objectAtIndex: 0 /*@"width"*/] doubleValue]; 93 | res.height = [(NSNumber *)[r objectAtIndex: 1 /*@"height"*/] doubleValue]; 94 | } 95 | [self setResolution: res]; 96 | 97 | 98 | _machine_media = MediaFromDictionary([d objectForKey: @"media"]); 99 | 100 | _machine_data = d; 101 | 102 | for (unsigned i = 0; i < MAX_SLOTS; ++i) { 103 | _slot_media[i] = EmptyMedia; 104 | } 105 | 106 | extern NSArray *BuildSlots(NSString *name, NSDictionary *data); 107 | _root = BuildSlots(_machine, d); 108 | objc_setAssociatedObject(_outlineView, &RootKey, _root, OBJC_ASSOCIATION_RETAIN); 109 | 110 | for (Slot *item in _root) { 111 | NSString *name = [item name]; 112 | NSInteger index = [item index] - 1; 113 | if (index < 0 || index >= MAX_SLOTS) continue; 114 | 115 | if ([item type] == kSlotBIOS) continue; 116 | 117 | NSString *v = [_slotValues objectForKey: name]; 118 | if (v) { 119 | [item selectValue: v]; 120 | } 121 | // TODO -- reset to default index??? 122 | 123 | _slot_media[index] = [item selectedMedia]; 124 | } 125 | 126 | 127 | [_outlineView reloadData]; 128 | if (!_loadingBookmark) { 129 | [self rebuildMedia]; 130 | [self rebuildArgs]; 131 | } 132 | } 133 | 134 | -(void)setMachine: (NSString *)machine { 135 | if (_machine == machine) return; 136 | if (_machine && machine && [machine compare: _machine] == NSOrderedSame) return; 137 | _machine = machine; 138 | 139 | if (!machine) { 140 | [self resetMachine]; 141 | return; 142 | } 143 | [self loadMachine]; 144 | } 145 | 146 | 147 | 148 | 149 | -(void)rebuildMedia { 150 | 151 | Media media = EmptyMedia; 152 | 153 | for (unsigned i = 0; i < MAX_SLOTS; ++i) { 154 | 155 | MediaAdd(&media, &_slot_media[i]); 156 | } 157 | // machine media last. 158 | MediaAdd(&media, &_machine_media); 159 | 160 | [self setMedia: media]; 161 | } 162 | 163 | 164 | -(void)rebuildArgs { 165 | 166 | NSMutableArray *args = [NSMutableArray new]; 167 | 168 | for (Slot *item in _root) { 169 | 170 | NSArray *x = [item args]; 171 | if (x) [args addObjectsFromArray: x]; 172 | } 173 | 174 | [self setArgs: args]; 175 | } 176 | 177 | - (IBAction)menuChanged:(NSPopUpButton *)sender { 178 | 179 | BOOL direct = YES; 180 | NSInteger index = [sender tag]; 181 | 182 | if (index < 0x10000) { 183 | direct = YES; 184 | } else { 185 | direct = NO; 186 | index &= ~0x10000; 187 | } 188 | index--; 189 | if (index < 0 || index >= MAX_SLOTS) return; // 190 | 191 | SlotOption *o = [[sender selectedItem] representedObject]; 192 | Slot *item = [_root objectAtIndex: index]; 193 | 194 | if (direct && [item type] != kSlotBIOS) { 195 | NSString *name = [item name]; 196 | NSString *value = [o value]; 197 | [_slotValues setObject: value forKey: name]; 198 | } 199 | 200 | Media media = [item selectedMedia]; 201 | if (!MediaEqual(&media, &_slot_media[index])) { 202 | _slot_media[index] = media; 203 | [self rebuildMedia]; 204 | } 205 | 206 | // needs to reload children if expanded. 207 | #ifdef SLOT_TREE 208 | if (direct) { 209 | BOOL rc = ([_outlineView isItemExpanded: item]); 210 | [_outlineView reloadItem: item reloadChildren: rc]; 211 | } 212 | #endif 213 | [self rebuildArgs]; 214 | } 215 | - (IBAction)hamburger:(id)sender { 216 | 217 | #if 0 218 | if ([_popover isShown]) { 219 | [_popover close]; 220 | } 221 | #endif 222 | 223 | NSInteger index = [sender tag]; 224 | if (index <= 0 || index >= 0x10000) return; 225 | index--; 226 | Slot *item = [_root objectAtIndex: index]; 227 | 228 | NSArray *children = [item selectedChildren]; 229 | objc_setAssociatedObject(_childOutlineView, &RootKey, children, OBJC_ASSOCIATION_RETAIN); 230 | if (!children) return; 231 | 232 | [_childOutlineView reloadData]; 233 | NSSize size = [_popover contentSize]; 234 | if (size.width < 200) size.width = 250; 235 | size = [_childOutlineView sizeThatFits: size]; 236 | size.height += 40; 237 | [_popover setContentSize: size]; 238 | 239 | [_popover showRelativeToRect: [sender bounds] 240 | ofView: sender 241 | preferredEdge: NSRectEdgeMaxY]; 242 | } 243 | 244 | -(IBAction)resetSlots:(id)sender { 245 | 246 | 247 | //_slots_explicit = 0; 248 | for (unsigned i = 0; i < MAX_SLOTS; ++i) { 249 | _slot_media[i] = EmptyMedia; 250 | } 251 | for (Slot *item in _root) { 252 | NSString *name = [item name]; 253 | 254 | [_slotValues removeObjectForKey: name]; 255 | 256 | [item reset]; 257 | // if children, reset them too... 258 | NSInteger index = [item index] - 1; 259 | if (index < 0) continue; 260 | _slot_media[index] = [item selectedMedia]; 261 | } 262 | 263 | #ifdef SLOT_TREE 264 | [_outlineView reloadData]; 265 | #endif 266 | if (!_loadingBookmark) { 267 | [self rebuildMedia]; 268 | [self rebuildArgs]; 269 | } 270 | } 271 | 272 | @end 273 | 274 | 275 | @implementation SlotViewController (OutlineView) 276 | 277 | 278 | - (NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item { 279 | 280 | NSArray *root = objc_getAssociatedObject(outlineView, &RootKey); 281 | if (!item) return [root count]; 282 | 283 | #ifdef SLOT_TREE 284 | NSArray *tmp = [(Slot *)item selectedChildren]; 285 | return [tmp count]; 286 | #endif 287 | return 0; 288 | } 289 | 290 | - (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item { 291 | NSArray *root = objc_getAssociatedObject(outlineView, &RootKey); 292 | 293 | if (!item) return [root objectAtIndex: index]; 294 | #ifdef SLOT_TREE 295 | NSArray *tmp = [(Slot *)item selectedChildren]; 296 | return [tmp objectAtIndex: index]; 297 | #endif 298 | return nil; 299 | } 300 | 301 | 302 | - (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item { 303 | 304 | #ifdef SLOT_TREE 305 | if (!item) return NO; 306 | NSArray *tmp = [(Slot *)item selectedChildren]; 307 | return [tmp count] > 0; 308 | #else 309 | return NO; 310 | #endif 311 | } 312 | 313 | 314 | 315 | - (NSView *)outlineView:(NSOutlineView *)outlineView viewForTableColumn:(NSTableColumn *)tableColumn item:(Slot *)item { 316 | 317 | SlotTableCellView *v = [outlineView makeViewWithIdentifier: @"MenuCell" owner: self]; 318 | 319 | [item prepareView: v]; 320 | 321 | return v; 322 | } 323 | 324 | 325 | @end 326 | 327 | 328 | @implementation SlotViewController (Bookmark) 329 | 330 | 331 | -(void)willLoadBookmark:(NSDictionary *)bookmark { 332 | _loadingBookmark = YES; 333 | [self setMachine: nil]; 334 | } 335 | -(void)didLoadBookmark:(NSDictionary *)bookmark { 336 | _loadingBookmark = NO; 337 | 338 | [self rebuildArgs]; 339 | } 340 | 341 | -(BOOL)loadBookmark: (NSDictionary *)bookmark { 342 | 343 | NSDictionary *dict = [bookmark objectForKey: @"slots"]; 344 | 345 | [self setMachine: [bookmark objectForKey: @"machine"]]; 346 | [self resetSlots: nil]; 347 | 348 | for (Slot *item in _root) { 349 | [item reserialize: dict]; 350 | 351 | NSInteger index = [item index] - 1; 352 | if (index >= 0 && index < MAX_SLOTS) { 353 | 354 | NSString *name = [item name]; 355 | [_slotValues removeObjectForKey: name]; 356 | 357 | if ([item defaultIndex] != [item selectedIndex]) { 358 | NSString *v = [item selectedValue]; 359 | if (v) [_slotValues setObject: v forKey: name]; 360 | } 361 | 362 | _slot_media[index] = [item selectedMedia]; 363 | } 364 | } 365 | 366 | // need to do it here so it propogate to media view. 367 | [self rebuildMedia]; 368 | return YES; 369 | } 370 | -(BOOL)saveBookmark: (NSMutableDictionary *)bookmark { 371 | 372 | NSMutableDictionary *slots = [NSMutableDictionary new]; 373 | for (Slot *item in _root) { 374 | NSDictionary *d = [item serialize]; 375 | [slots addEntriesFromDictionary: d]; 376 | } 377 | 378 | [bookmark setObject: slots forKey: @"slots"]; 379 | return YES; 380 | } 381 | 382 | 383 | 384 | @end 385 | -------------------------------------------------------------------------------- /Ample/SoftwareList.h: -------------------------------------------------------------------------------- 1 | // 2 | // SoftwareList.h 3 | // Ample 4 | // 5 | // Created by Kelvin Sherlock on 3/28/2021. 6 | // Copyright © 2021 Kelvin Sherlock. All rights reserved. 7 | // 8 | 9 | #ifndef SoftwareList_h 10 | #define SoftwareList_h 11 | 12 | #import 13 | #import "AutocompleteControl.h" 14 | 15 | 16 | @interface SoftwareList : NSObject 17 | @property NSString *name; 18 | @property NSString *title; 19 | @property NSArray *items; 20 | @property NSString *notes; 21 | 22 | -(SoftwareList *)filter: (NSString *)filter; 23 | 24 | @end 25 | 26 | @interface Software : NSObject 27 | @property NSString *name; 28 | @property NSString *title; 29 | @property NSString *compatibility; 30 | @property NSString *list; 31 | @property NSString *notes; 32 | 33 | -(NSString *)fullName; 34 | 35 | @property NSString *searchTitle; 36 | 37 | @end 38 | 39 | @interface SoftwareSet : NSObject 40 | 41 | +(instancetype)softwareSetForMachine: (NSString *)machine; 42 | +(void)invalidate; 43 | 44 | -(BOOL)nameIsUnique: (NSString *)name; 45 | 46 | -(NSString *)nameForSoftware: (Software *)software; 47 | -(Software *)softwareForName: (NSString *)name; 48 | 49 | -(BOOL)hasSoftware: (Software *)software; 50 | @end 51 | 52 | 53 | //NSArray *SoftwareListForMachine(NSString *machine); 54 | 55 | 56 | #endif /* SoftwareList_h */ 57 | -------------------------------------------------------------------------------- /Ample/TableCellView.h: -------------------------------------------------------------------------------- 1 | // 2 | // TableCellView.h 3 | // Ample 4 | // 5 | // Created by Kelvin Sherlock on 9/13/2020. 6 | // Copyright © 2020 Kelvin Sherlock. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //NS_ASSUME_NONNULL_BEGIN 12 | 13 | 14 | enum { 15 | kIndexFloppy8 = 0, 16 | kIndexFloppy525, 17 | kIndexFloppy35, 18 | kIndexHardDrive, 19 | kIndexCDROM, 20 | kIndexCassette, 21 | kIndexDiskImage, 22 | kIndexBitBanger, 23 | kIndexMidiIn, 24 | kIndexMidiOut, 25 | kIndexPicture, // computer eyes -pic, .png only. 26 | kIndexROM, 27 | // kIndexPrintout // -prin, .prn extension only? 28 | 29 | kIndexLast 30 | }; 31 | #define CATEGORY_COUNT 12 32 | static_assert(kIndexLast == CATEGORY_COUNT, "Invalid Category Count"); 33 | 34 | 35 | @interface MediaTableCellView : NSTableCellView 36 | @property (weak) IBOutlet NSButton *ejectButton; 37 | @property (weak) IBOutlet NSImageView *dragHandle; 38 | @property BOOL movable; 39 | 40 | -(void)prepareView: (NSInteger)category; 41 | @end 42 | 43 | @interface PathTableCellView : MediaTableCellView 44 | @property (weak) IBOutlet NSPathControl *pathControl; 45 | @end 46 | 47 | 48 | @interface MidiTableCellView : MediaTableCellView 49 | @property (weak) IBOutlet NSPopUpButton *popUpButton; 50 | @end 51 | 52 | //NS_ASSUME_NONNULL_END 53 | -------------------------------------------------------------------------------- /Ample/TableCellView.m: -------------------------------------------------------------------------------- 1 | // 2 | // TableCellView.m 3 | // Ample 4 | // 5 | // Created by Kelvin Sherlock on 9/13/2020. 6 | // Copyright © 2020 Kelvin Sherlock. All rights reserved. 7 | // 8 | 9 | #import "TableCellView.h" 10 | #import "MidiManager.h" 11 | #import "Menu.h" 12 | 13 | 14 | @implementation MediaTableCellView 15 | #if 0 16 | { 17 | NSTrackingRectTag _trackingRect; 18 | } 19 | #endif 20 | -(void)awakeFromNib { 21 | 22 | // need to do it here for 10.11 compatibility. 23 | 24 | if (@available(macOS 10.14, *)) { 25 | NSValueTransformer *t; 26 | NSDictionary *options; 27 | 28 | t = [NSValueTransformer valueTransformerForName: @"ValidColorTransformer"]; 29 | options = @{ NSValueTransformerBindingOption: t}; 30 | [_ejectButton bind: @"contentTintColor" toObject: self withKeyPath: @"objectValue.valid" options: options]; 31 | } else { 32 | // El Capitan TODO... 33 | } 34 | 35 | } 36 | 37 | -(void)prepareView: (NSInteger)category { 38 | } 39 | 40 | #if 0 41 | -(void)awakeFromNib { 42 | 43 | // this is apparently necessary for setTintColor to work. 44 | NSImage *img; 45 | img = [_ejectButton image]; 46 | [img setTemplate: YES]; 47 | img = [_ejectButton alternateImage]; 48 | [img setTemplate: YES]; 49 | } 50 | #endif 51 | 52 | /* mouse tracking to enable/disable dragger image -- no longer used.*/ 53 | #if 0 54 | -(void)viewDidMoveToSuperview { 55 | if (_trackingRect) { 56 | [self removeTrackingRect: _trackingRect]; 57 | } 58 | NSRect rect = [_dragHandle frame]; 59 | _trackingRect = [self addTrackingRect: rect owner: self userData: NULL assumeInside:NO]; 60 | } 61 | 62 | -(void)mouseEntered:(NSEvent *)event { 63 | [_dragHandle setHidden: NO]; 64 | } 65 | 66 | -(void)mouseExited:(NSEvent *)event { 67 | [_dragHandle setHidden: YES]; 68 | } 69 | #endif 70 | 71 | @end 72 | 73 | @implementation PathTableCellView 74 | 75 | -(void)prepareView: (NSInteger)category { 76 | [_pathControl setTag: category + 1]; 77 | } 78 | 79 | - (void)pathControl:(NSPathControl *)pathControl willPopUpMenu:(NSMenu *)menu { 80 | // if this is an output path, replace the "choose..." button with a save panel. 81 | NSMenuItem *item = [menu itemAtIndex: 0]; 82 | if (item) { 83 | [item setTarget: self]; 84 | [item setAction: @selector(choosePath:)]; 85 | } 86 | } 87 | 88 | -(IBAction)choosePath:(id)sender { 89 | NSPathControl *pc = _pathControl; 90 | NSURL *url = [pc URL]; 91 | 92 | NSSavePanel *p = [NSSavePanel savePanel]; 93 | 94 | if (url) { 95 | NSFileManager *fm = [NSFileManager defaultManager]; 96 | BOOL dir = NO; 97 | NSString *str = [NSString stringWithCString: [url fileSystemRepresentation] encoding: NSUTF8StringEncoding]; 98 | [fm fileExistsAtPath: str isDirectory: &dir]; 99 | 100 | if (!dir) { 101 | [p setNameFieldStringValue: [str lastPathComponent]]; 102 | url = [url URLByDeletingLastPathComponent]; 103 | } 104 | [p setDirectoryURL: url]; 105 | } 106 | [p setExtensionHidden: NO]; 107 | 108 | [p beginWithCompletionHandler: ^(NSModalResponse response){ 109 | if (response != NSModalResponseOK) return; 110 | NSURL *url = [p URL]; 111 | [pc setURL: url]; 112 | }]; 113 | 114 | } 115 | 116 | @end 117 | 118 | 119 | @interface EmptyStringTransformer : NSValueTransformer 120 | 121 | @end 122 | 123 | static NSString *kNone = @"—None—"; 124 | 125 | @implementation EmptyStringTransformer 126 | 127 | +(void)load { 128 | [self setValueTransformer: [self new] forName: @"EmptyStringTransformer"]; 129 | } 130 | 131 | + (Class)transformedValueClass { 132 | return [NSString class]; 133 | } 134 | + (BOOL)allowsReverseTransformation { 135 | return YES; 136 | } 137 | - (id)transformedValue:(id)value { 138 | if (value == nil) return kNone; 139 | if ([kNone isEqualToString: value]) return nil; 140 | return value; 141 | } 142 | 143 | @end 144 | 145 | @implementation MidiTableCellView { 146 | NSInteger _category; 147 | } 148 | 149 | /* binding should be able to handle the menu but i couldn't make it work. */ 150 | 151 | 152 | -(void)prepareView: (NSInteger)category { 153 | _category = category; 154 | 155 | // 10.11 + doesn't need to remove the observer in the -dealloc 156 | NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; 157 | [nc addObserver: self selector: @selector(midiChanged:) name: category == kIndexMidiIn ? kMidiSourcesChangedNotification : kMidiDestinationsChangedNotification object: nil]; 158 | 159 | [self updateMenus: NO]; 160 | } 161 | 162 | -(void)updateMenus: (BOOL)notification { 163 | NSMenu *menu = [_popUpButton menu]; 164 | MidiManager *mgr = [MidiManager sharedManager]; 165 | 166 | NSArray *array = _category == kIndexMidiIn ? [mgr sources] : [mgr destinations]; 167 | 168 | NSString *selected = [[_popUpButton selectedItem] title]; 169 | [menu removeAllItems]; 170 | int selectedIndex = -1; 171 | NSMenuItem *item; 172 | 173 | item = [[NSMenuItem alloc] initWithTitle: kNone action: NULL keyEquivalent: @""]; 174 | [item setAttributedTitle: ItalicMenuString(kNone)]; 175 | [menu addItem: item]; 176 | selectedIndex = 0; 177 | #if 0 178 | if (!selected || [@"" isEqualToString: selected]) { 179 | selectedIndex = 0; 180 | } 181 | #endif 182 | 183 | int ix = 1; 184 | for (NSString *s in array) { 185 | item = [[NSMenuItem alloc] initWithTitle: s action: NULL keyEquivalent: @""]; 186 | [item setRepresentedObject: s]; 187 | [menu addItem: item]; 188 | if ([s isEqualToString: selected]) { 189 | selectedIndex = ix; 190 | } 191 | ++ix; 192 | } 193 | 194 | // does this propogate? 195 | [_popUpButton selectItemAtIndex: selectedIndex]; 196 | if (notification) [_popUpButton sendAction: [_popUpButton action] to: [_popUpButton target]]; 197 | } 198 | 199 | -(void)midiChanged: (NSNotification *)notification { 200 | 201 | [self updateMenus: YES]; 202 | } 203 | 204 | 205 | -(void)prepareForReuse { 206 | NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; 207 | [nc removeObserver: self]; 208 | _category = 0; 209 | [super prepareForReuse]; 210 | } 211 | 212 | @end 213 | -------------------------------------------------------------------------------- /Ample/Transformers.h: -------------------------------------------------------------------------------- 1 | // 2 | // Transformers.h 3 | // Ample 4 | // 5 | // Created by Kelvin Sherlock on 9/13/2020. 6 | // Copyright © 2020 Kelvin Sherlock. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | void RegisterTransformers(void); 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | 15 | @interface FilePathTransformer : NSValueTransformer 16 | @end 17 | 18 | @interface FileSizeTransformer : NSValueTransformer 19 | @end 20 | 21 | @interface ValidColorTransformer : NSValueTransformer 22 | @end 23 | 24 | @interface StringNotEmptyTransformer : NSValueTransformer 25 | @end 26 | 27 | 28 | NS_ASSUME_NONNULL_END 29 | -------------------------------------------------------------------------------- /Ample/Transformers.m: -------------------------------------------------------------------------------- 1 | // 2 | // Transformers.m 3 | // Ample 4 | // 5 | // Created by Kelvin Sherlock on 9/13/2020. 6 | // Copyright © 2020 Kelvin Sherlock. All rights reserved. 7 | // 8 | 9 | #import "Transformers.h" 10 | 11 | #import 12 | 13 | @implementation FilePathTransformer 14 | 15 | + (Class)transformedValueClass { 16 | return [NSString class]; 17 | } 18 | 19 | + (BOOL)allowsReverseTransformation { 20 | return NO; 21 | } 22 | 23 | - (id)transformedValue:(id)value { 24 | if (!value) return value; 25 | 26 | return [(NSString *)value lastPathComponent]; 27 | } 28 | 29 | @end 30 | 31 | @implementation FileSizeTransformer 32 | 33 | + (Class)transformedValueClass { 34 | return [NSString class]; 35 | } 36 | 37 | + (BOOL)allowsReverseTransformation { 38 | return NO; 39 | } 40 | 41 | - (id)transformedValue:(id)value { 42 | if (!value) return value; 43 | if (![value respondsToSelector: @selector(integerValue)]) { 44 | [NSException raise: NSInternalInconsistencyException 45 | format: @"Value (%@) does not respond to -integerValue.", 46 | [value class]]; 47 | } 48 | NSInteger size = [(NSNumber *)value integerValue]; 49 | 50 | if (size < 0) return nil; 51 | if (size < 1024*1024) return [NSString stringWithFormat: @"%.1fKB", (float)size / 1024]; 52 | if (size < 1024*1024*1024) return [NSString stringWithFormat: @"%.1fMB", (float)size / (1024*1024)]; 53 | 54 | return [NSString stringWithFormat: @"%.1fGB", (float)size / (1024*1024*1024)]; 55 | } 56 | 57 | @end 58 | 59 | 60 | @implementation ValidColorTransformer 61 | + (BOOL)allowsReverseTransformation { 62 | return NO; 63 | } 64 | + (Class)transformedValueClass { 65 | return [NSColor class]; 66 | } 67 | 68 | - (id)transformedValue:(id)value { 69 | BOOL valid = [(NSNumber *)value boolValue]; 70 | return valid ? nil : [NSColor systemRedColor]; 71 | } 72 | 73 | @end 74 | 75 | 76 | @implementation StringNotEmptyTransformer 77 | + (BOOL)allowsReverseTransformation { 78 | return NO; 79 | } 80 | + (Class)transformedValueClass { 81 | return [NSNumber class]; 82 | } 83 | 84 | - (id)transformedValue:(id)value { 85 | NSUInteger length = [(NSString *)value length]; 86 | return [NSNumber numberWithBool: length ? YES : NO]; 87 | } 88 | 89 | @end 90 | 91 | 92 | void RegisterTransformers(void) { 93 | 94 | NSValueTransformer *t; 95 | t = [FileSizeTransformer new]; 96 | [NSValueTransformer setValueTransformer: t forName: @"FileSizeTransformer"]; 97 | 98 | t = [FilePathTransformer new]; 99 | [NSValueTransformer setValueTransformer: t forName: @"FilePathTransformer"]; 100 | 101 | t = [ValidColorTransformer new]; 102 | [NSValueTransformer setValueTransformer: t forName: @"ValidColorTransformer"]; 103 | 104 | t = [StringNotEmptyTransformer new]; 105 | [NSValueTransformer setValueTransformer: t forName: @"StringNotEmptyTransformer"]; 106 | 107 | } 108 | -------------------------------------------------------------------------------- /Ample/TransparentScroller.h: -------------------------------------------------------------------------------- 1 | // 2 | // TransparentScroller.h 3 | // Ample 4 | // 5 | // Created by Kelvin Sherlock on 9/4/2020. 6 | // Copyright © 2020 Kelvin Sherlock. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface TransparentScroller : NSScroller 14 | 15 | @property NSColor *backgroundColor; 16 | 17 | @end 18 | 19 | NS_ASSUME_NONNULL_END 20 | -------------------------------------------------------------------------------- /Ample/TransparentScroller.m: -------------------------------------------------------------------------------- 1 | // 2 | // TransparentScroller.m 3 | // Ample 4 | // 5 | // Created by Kelvin Sherlock on 9/4/2020. 6 | // Copyright © 2020 Kelvin Sherlock. All rights reserved. 7 | // 8 | 9 | #import "TransparentScroller.h" 10 | 11 | @implementation TransparentScroller 12 | 13 | - (void)drawRect:(NSRect)dirtyRect { 14 | //[super drawRect:dirtyRect]; 15 | [[NSColor clearColor] set]; 16 | NSRectFill(dirtyRect); 17 | 18 | #if 0 19 | NSColor *color = _backgroundColor; 20 | if (color) { 21 | [color setFill]; 22 | NSRectFill(dirtyRect); 23 | } 24 | #endif 25 | [self drawKnob]; 26 | } 27 | 28 | @end 29 | -------------------------------------------------------------------------------- /Ample/images/caution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ksherlock/ample/60cb69df42b436de43bcaa87a895c28212ee9757/Ample/images/caution.png -------------------------------------------------------------------------------- /Ample/images/caution@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ksherlock/ample/60cb69df42b436de43bcaa87a895c28212ee9757/Ample/images/caution@2x.png -------------------------------------------------------------------------------- /Ample/images/caution@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ksherlock/ample/60cb69df42b436de43bcaa87a895c28212ee9757/Ample/images/caution@3x.png -------------------------------------------------------------------------------- /Ample/images/drag-handle-4x10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ksherlock/ample/60cb69df42b436de43bcaa87a895c28212ee9757/Ample/images/drag-handle-4x10.png -------------------------------------------------------------------------------- /Ample/images/drag-handle-4x10@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ksherlock/ample/60cb69df42b436de43bcaa87a895c28212ee9757/Ample/images/drag-handle-4x10@2x.png -------------------------------------------------------------------------------- /Ample/images/drag-handle-4x10@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ksherlock/ample/60cb69df42b436de43bcaa87a895c28212ee9757/Ample/images/drag-handle-4x10@3x.png -------------------------------------------------------------------------------- /Ample/images/eject-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ksherlock/ample/60cb69df42b436de43bcaa87a895c28212ee9757/Ample/images/eject-16x16.png -------------------------------------------------------------------------------- /Ample/images/eject-16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ksherlock/ample/60cb69df42b436de43bcaa87a895c28212ee9757/Ample/images/eject-16x16@2x.png -------------------------------------------------------------------------------- /Ample/images/eject-16x16@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ksherlock/ample/60cb69df42b436de43bcaa87a895c28212ee9757/Ample/images/eject-16x16@3x.png -------------------------------------------------------------------------------- /Ample/images/eject-hover-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ksherlock/ample/60cb69df42b436de43bcaa87a895c28212ee9757/Ample/images/eject-hover-16x16.png -------------------------------------------------------------------------------- /Ample/images/eject-hover-16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ksherlock/ample/60cb69df42b436de43bcaa87a895c28212ee9757/Ample/images/eject-hover-16x16@2x.png -------------------------------------------------------------------------------- /Ample/images/eject-hover-16x16@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ksherlock/ample/60cb69df42b436de43bcaa87a895c28212ee9757/Ample/images/eject-hover-16x16@3x.png -------------------------------------------------------------------------------- /Ample/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // Ample 4 | // 5 | // Created by Kelvin Sherlock on 8/16/2020. 6 | // Copyright © 2020 Kelvin Sherlock. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | int main(int argc, const char * argv[]) { 12 | @autoreleasepool { 13 | // Setup code that might create autoreleased objects goes here. 14 | } 15 | return NSApplicationMain(argc, argv); 16 | } 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ample 2 | 3 | A slightly more user-friendly front-end for using MAME as an Apple II emulator. Requires Mac OS X 10.11+ 4 | 5 | ![](screenshots/2021-07-01.png) 6 | 7 | 8 | A custom version of MAME is included. (Only Apple 1/2/3 and Macintosh emulators are included in the custom version). 9 | 10 | A note for Macintosh emulation: 11 | 12 | * Images selected in the “Hard Drives” section must have a full partition table with Mac drivers (this will always be the case) 13 | * "Hard Disk Images" (enabled with the NuBus Disk Image Pseudo-Card in a slot) can be used to mount standard HFS images without the need of a partition table or driver (this will always be the case) 14 | 15 | 16 | -------------------------------------------------------------------------------- /embedded/Host.FST.po: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ksherlock/ample/60cb69df42b436de43bcaa87a895c28212ee9757/embedded/Host.FST.po -------------------------------------------------------------------------------- /embedded/Host.MLI.po: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ksherlock/ample/60cb69df42b436de43bcaa87a895c28212ee9757/embedded/Host.MLI.po -------------------------------------------------------------------------------- /embedded/README.md: -------------------------------------------------------------------------------- 1 | 2 | This folder should contain SDL2.framework and a mame64 executable. These will be included in the build. 3 | 4 | * [SDL2](http://libsdl.org/download-2.0.php) 5 | * [MAME](https://github.com/mamedev/mame) (requires building from source) 6 | 7 | Not tested, but perhaps you could also download a [pre-built MAME](https://wiki.mamedev.org/index.php/SDL_Supported_Platforms) and use `install_name_tool` to fix the rpath. 8 | 9 | Alternatively, adjust the xcode project to not embed them. 10 | 11 | 12 | Building MAME: 13 | 14 | This will build a subset of MAME which only includes apple2 support. 15 | 16 | git clone mame ... 17 | cd mame 18 | make SOURCES=src/mame/drivers/apple1.cpp,src/mame/drivers/apple2.cpp,src/mame/drivers/apple2e.cpp,src/mame/drivers/apple2gs.cpp,src/mame/drivers/apple3.cpp SDL_FRAMEWORK_PATH=`pwd`/.. 19 | 20 | you can use `$LDFLAGS` to set the rpath (`LDFLAGS="-rpath @executable_path/../Frameworks" make ...`) or set it after with the `install_name_tool` tool - ``install_name_tool -add_rpath @executable_path/../Frameworks mame64` 21 | 22 | 23 | -------------------------------------------------------------------------------- /embedded/download-sdl.sh: -------------------------------------------------------------------------------- 1 | 2 | #!/bin/sh 3 | VERSION=2.26.3 4 | DMG=SDL2-${VERSION}.dmg 5 | URL=https://www.libsdl.org/release/SDL2-${VERSION}.dmg 6 | FRAMEWORK=SDL2.framework 7 | 8 | if [ -e $FRAMEWORK ] ; then exit 0 ; fi 9 | 10 | if [ ! -e $DMG ] ; then curl -OL $URL ; fi 11 | 12 | hdiutil attach $DMG -noverify -nobrowse -mountpoint /Volumes/sdl_disk_image 13 | 14 | # cp -r /sdl_disk_image/$FRAMEWORK ./ 15 | ditto /Volumes/sdl_disk_image/$FRAMEWORK $FRAMEWORK 16 | hdiutil detach /Volumes/sdl_disk_image 17 | -------------------------------------------------------------------------------- /embedded/download-sparkle.sh: -------------------------------------------------------------------------------- 1 | 2 | #!/bin/sh 3 | 4 | VERSION=2.2.0 5 | TAR=Sparkle-${VERSION}.tar.xz 6 | URL=https://github.com/sparkle-project/Sparkle/releases/download/${VERSION}/Sparkle-${VERSION}.tar.xz 7 | FRAMEWORK=Sparkle.framework 8 | 9 | if [ -e $FRAMEWORK ] ; then exit 0 ; fi 10 | 11 | if [ ! -e $TAR ] ; then curl -OL $URL ; fi 12 | 13 | mkdir -p Sparkle-${VERSION} 14 | cd Sparkle-${VERSION} 15 | if [ ! -e $FRAMEWORK ] ; then tar xfz ../$TAR ; fi 16 | cd .. 17 | 18 | ditto Sparkle-${VERSION}/$FRAMEWORK $FRAMEWORK 19 | 20 | # older version of xcode need a Versions/A directory 21 | 22 | SW_VERSION=`sw_vers -productVersion` 23 | case $SW_VERSION in 24 | 10.14|10.14.*) 25 | if [ ! -e $FRAMEWORK/Versions/A ] ; then ln -s B $FRAMEWORK/Versions/A ; fi 26 | esac 27 | 28 | -------------------------------------------------------------------------------- /embedded/install_name_tool.pl: -------------------------------------------------------------------------------- 1 | use strict; 2 | use Getopt::Long; 3 | use Data::Dumper; 4 | 5 | my $file; 6 | my @rpaths; 7 | my $path; 8 | my $verbose = 0; 9 | my $help = 0; 10 | my $dry_run = 0; 11 | 12 | sub help($) { 13 | print("Usage: install_name_tool.pl [--dry-run] [--verbose] exec-file rpath\n"); 14 | exit(shift); 15 | } 16 | 17 | sub uniq { 18 | my %seen; 19 | grep !$seen{$_}++, @_; 20 | } 21 | 22 | GetOptions("help" => \$help, "verbose" => \$verbose, "dry-run" => \$dry_run); 23 | help(0) if $help; 24 | $verbose = 1 if $dry_run; 25 | 26 | help(1) unless scalar(@ARGV) == 2; 27 | ($file, $path) = @ARGV; 28 | 29 | 30 | open(my $fh, "-|", "otool", "-l", $file); 31 | 32 | 33 | # 34 | #Load command 33 35 | # cmd LC_RPATH 36 | # cmdsize 32 37 | # path ./Frameworks/ (offset 12) 38 | # 39 | # 40 | 41 | 42 | my $cmd = ''; 43 | while (<$fh>) { 44 | chomp; 45 | if ($_ =~ /^Load command/) { 46 | $cmd = ''; 47 | next; 48 | } 49 | if ($_ =~ /^\s+cmd ([A-Z_]+)$/) { 50 | $cmd = $1; 51 | next; 52 | } 53 | if ($cmd eq 'LC_RPATH' && $_ =~ /^\s+path (.+) \(offset \d+\)$/) { 54 | push(@rpaths, $1); 55 | } 56 | } 57 | close($fh); 58 | 59 | @rpaths = uniq(@rpaths); 60 | 61 | if ($verbose) { 62 | print "current rpaths:\n"; 63 | foreach(@rpaths) { 64 | print($_ . "\n"); 65 | } 66 | } 67 | 68 | my @args; 69 | 70 | # grrr... -change doesn't seem to work anymore. 71 | # equal or changeable. 72 | if (scalar @rpaths == 1) { 73 | exit(0) if $rpaths[0] eq $path; 74 | # push(@args, ("-change", ${rpaths[0]}, $path)) 75 | } 76 | #} else { 77 | if (1) { 78 | 79 | my @tmp; 80 | @tmp = grep {$_ ne $path } @rpaths; 81 | 82 | foreach (@tmp) { 83 | push(@args, ("-delete_rpath", $_)) 84 | } 85 | 86 | 87 | @tmp = grep {$_ eq $path } @rpaths; 88 | if (!scalar @tmp) { 89 | push(@args, ("-add_rpath", $path)); 90 | } 91 | } 92 | 93 | if (scalar @args) { 94 | print( join(' ', "install_name_tool", @args, $file) . "\n") if $verbose; 95 | exit(0) if $dry_run; 96 | system("install_name_tool", @args, $file); 97 | exit($?); 98 | } 99 | exit(0); 100 | -------------------------------------------------------------------------------- /embedded/mame64.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.cs.disable-library-validation 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /pty_shell/pty_shell.c: -------------------------------------------------------------------------------- 1 | 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | #define TTYDEFCHARS 21 | #include 22 | 23 | 24 | void usage(int rv) { 25 | 26 | fputs( 27 | "Usage: pty_shell [-T term] [-w] [-r] pty [command ...]\n" 28 | " -T term TERM (default vt100)\n" 29 | " -w Don't wait for child to finish\n" 30 | " -r Raw I/O\n" 31 | , stderr); 32 | exit(rv); 33 | } 34 | 35 | 36 | char *xsprintf(char *fmt, ...) { 37 | 38 | int ok; 39 | char *buffer = NULL; 40 | va_list ap; 41 | 42 | va_start(ap, fmt); 43 | ok = vasprintf(&buffer, fmt, ap); 44 | if (ok < 0) { 45 | errx(EX_SOFTWARE, "vasprintf failed"); 46 | } 47 | va_end(ap); 48 | return buffer; 49 | } 50 | 51 | /* re-create execve path search so we have better control */ 52 | /* return string may or may not be allocated */ 53 | char *findexe(char *name) { 54 | 55 | struct stat st; 56 | char *cp; 57 | int ok; 58 | 59 | char *path = getenv("PATH"); 60 | if (!path) path = _PATH_DEFPATH; 61 | 62 | if (!name || !*name) { 63 | errno = ENOENT; 64 | return NULL; 65 | } 66 | if (*name == '/') return name; 67 | if (strchr(name, '/')) { 68 | cp = realpath(name, NULL); 69 | return cp; 70 | } 71 | 72 | char *start = path; 73 | char *end = NULL; 74 | size_t l; 75 | 76 | for(;;) { 77 | end = strchr(start, ':'); 78 | if (!end) { 79 | /* last one */ 80 | l = strlen(start); 81 | } else { 82 | l = end - start; 83 | } 84 | if (l == 0) { 85 | /* current directory */ 86 | cp = realpath(name, NULL); 87 | } else { 88 | cp = xsprintf("%.*s/%s", l, start, name); 89 | } 90 | 91 | // fprintf(stderr, "%s\n", cp); 92 | ok = stat(cp, &st); 93 | 94 | if (ok >= 0 && (st.st_mode & S_IXUSR)) 95 | return cp; 96 | 97 | free(cp); 98 | 99 | if (!end) break; 100 | start = end + 1; 101 | } 102 | 103 | 104 | 105 | errno = ENOENT; 106 | return NULL; 107 | } 108 | 109 | 110 | void dup012(int fd) { 111 | dup2(fd, 0); 112 | dup2(fd, 1); 113 | dup2(fd, 2); 114 | if (fd > 2) close(fd); 115 | } 116 | 117 | void execute(int fd, char *path, char **argv, char **env) { 118 | 119 | int ok; 120 | int err_fd = fcntl(STDERR_FILENO, F_DUPFD_CLOEXEC, 0); 121 | 122 | // ok = 0; dup012(fd); 123 | ok = login_tty(fd); 124 | if (ok < 0) { 125 | dprintf(err_fd, "%s: login_tty: %s\n", 126 | getprogname(), strerror(errno) 127 | ); 128 | _exit(EX_OSERR); 129 | } 130 | execve(path, argv, env); 131 | 132 | dprintf(err_fd, "%s: execve %s: %s\n", 133 | getprogname(), path, strerror(errno) 134 | ); 135 | 136 | _exit(EX_OSERR); 137 | } 138 | 139 | 140 | pid_t child_pid; 141 | int pty_fd; 142 | void sig_handler(int sig, siginfo_t *info, void *context) { 143 | 144 | if (sig == SIGINFO || sig == SIGUSR1) { 145 | int rlen, wlen; 146 | rlen = wlen = 0; 147 | 148 | if (pty_fd > 0) { 149 | char buffer[128]; 150 | int n,x; 151 | 152 | ioctl(pty_fd, TIOCOUTQ, &wlen); 153 | ioctl(pty_fd, FIONREAD, &rlen); 154 | 155 | if (rlen > 9999) rlen = 9999; 156 | if (wlen > 9999) wlen = 9999; 157 | 158 | memcpy(buffer, "child pid: read queue: write queue: \n", 58); 159 | 160 | 161 | n = 16; 162 | x = child_pid; 163 | do { 164 | buffer[n--] = '0' + (x %10); 165 | x /= 10; 166 | } while(x); 167 | 168 | n = 35; 169 | x = rlen; 170 | do { 171 | buffer[n--] = '0' + (x %10); 172 | x /= 10; 173 | } while(x); 174 | 175 | n = 55; 176 | x = wlen; 177 | do { 178 | buffer[n--] = '0' + (x %10); 179 | x /= 10; 180 | } while(x); 181 | 182 | write(STDERR_FILENO, buffer, 58); 183 | 184 | } 185 | 186 | return; 187 | } 188 | if (sig == SIGHUP || sig == SIGINT || sig == SIGTERM) { 189 | /* pass to child */ 190 | if (child_pid >= 0) { 191 | kill(child_pid, sig); 192 | } 193 | struct sigaction sa; 194 | memset(&sa, 0, sizeof(sa)); 195 | sigemptyset(&sa.sa_mask); 196 | sa.sa_handler = SIG_DFL; 197 | sigaction(sig, &sa, NULL); 198 | kill(getpid(), sig); 199 | _exit(1); 200 | } 201 | 202 | } 203 | 204 | int main(int argc, char **argv) { 205 | 206 | int c; 207 | int fd; 208 | pid_t pid; 209 | int ok, i; 210 | char *pty; 211 | char *term = "vt100"; 212 | char *path = NULL; 213 | 214 | 215 | struct winsize ws = { 24, 80, 0, 0 }; 216 | struct termios tios; 217 | struct sigaction sa; 218 | 219 | 220 | char *env[10]; 221 | int flag_w = 0; 222 | // int flag_i = 0; 223 | int flag_f = 0; 224 | int flag_r = 0; 225 | int flag_v = 0; 226 | 227 | 228 | while ((c = getopt(argc, argv, "T:rwhv")) != -1) { 229 | switch(c) { 230 | // case 'f': flag_f = 1; break; 231 | case 'r': flag_r = 1; break; 232 | case 'w': flag_w = 1; break; 233 | case 'v': flag_v = 1; break; 234 | case 'h': usage(0); 235 | case 'T': 236 | term = optarg; 237 | break; 238 | 239 | default: 240 | exit(EX_USAGE); 241 | } 242 | } 243 | 244 | argc -= optind; 245 | argv += optind; 246 | 247 | // pty [optional command] 248 | 249 | if (argc < 1) { 250 | usage(EX_USAGE); 251 | } 252 | 253 | 254 | /* n.b. - with nonblock, fd can close before all data sent */ 255 | pty = argv[0]; 256 | fd = open(pty, O_RDWR | /* O_NONBLOCK | */ O_CLOEXEC); 257 | if (fd < 0) { 258 | err(EX_NOINPUT, "open %s", pty); 259 | } 260 | pty_fd = fd; 261 | 262 | --argc; 263 | ++argv; 264 | 265 | 266 | memset(&tios, 0, sizeof(tios)); 267 | memcpy(tios.c_cc, ttydefchars, sizeof(ttydefchars)); 268 | if (flag_r) { 269 | cfmakeraw(&tios); 270 | } else { 271 | tios.c_oflag = TTYDEF_OFLAG; 272 | tios.c_lflag = TTYDEF_LFLAG; 273 | tios.c_iflag = TTYDEF_IFLAG; 274 | tios.c_cflag = TTYDEF_CFLAG; 275 | } 276 | tios.c_ispeed = tios.c_ospeed = B9600; 277 | 278 | 279 | /* verify it's pty? */ 280 | 281 | ok = tcsetattr(fd, TCSAFLUSH, &tios); 282 | ok = ioctl(fd, TIOCSWINSZ, (void *)&ws); 283 | 284 | 285 | /* todo - option to retain environment? */ 286 | i = 0; 287 | env[i++] = "LANG=C"; 288 | env[i++] = xsprintf("TERM=%s", term); 289 | env[i++] = "COLUMNS=80"; 290 | env[i++] = "LINES=24"; 291 | if (argc) { 292 | char *cp; 293 | 294 | cp = getenv("HOME"); 295 | if (cp) { 296 | env[i++] = xsprintf("HOME=%s", cp); 297 | } 298 | } 299 | env[i] = 0; 300 | 301 | 302 | if (argc) { 303 | path = findexe(argv[0]); 304 | if (!path) { 305 | errx(EX_OSERR, "Unable to find %s", argv[0]); 306 | } 307 | argv[0] = basename(argv[0]); 308 | } else { 309 | /* -p: don't discard environment */ 310 | static char *args[] = { 311 | "login", 312 | "-pf", 313 | "", 314 | NULL 315 | }; 316 | char *login; 317 | 318 | login = getlogin(); 319 | if (!login) { 320 | errx(EX_OSERR, "getlogin() failed."); 321 | } 322 | 323 | path = "/usr/bin/login"; 324 | args[2] = login; 325 | argv = args; 326 | } 327 | 328 | /* n.b. - login_tty will fail unless root :/ */ 329 | if (flag_f) { 330 | /* foreground */ 331 | execute(fd, path, argv, env); 332 | exit(0); 333 | } 334 | 335 | pid = fork(); 336 | if (pid < 0) { 337 | close(fd); 338 | err(EX_OSERR, "fork"); 339 | } 340 | if (!pid) { 341 | /* child */ 342 | 343 | execute(fd, path, argv, env); 344 | } 345 | child_pid = pid; 346 | 347 | memset(&sa, 0, sizeof(sa)); 348 | sa.sa_flags = SA_SIGINFO | SA_RESTART; 349 | sa.sa_sigaction = sig_handler; 350 | sigfillset(&sa.sa_mask); 351 | 352 | sigaction(SIGINFO, &sa, NULL); 353 | sigaction(SIGUSR1, &sa, NULL); 354 | sigaction(SIGHUP, &sa, NULL); 355 | sigaction(SIGINT, &sa, NULL); 356 | sigaction(SIGTERM, &sa, NULL); 357 | 358 | /* wait for the child so data isn't lost. */ 359 | if (!flag_w) { 360 | pid_t ok; 361 | int st; 362 | 363 | printf("Waiting on child %d\n", (int)pid); 364 | 365 | for(;;) { 366 | ok = waitpid(pid, &st, 0); 367 | if (ok < 0) { 368 | if (errno == EINTR) { 369 | continue; 370 | } 371 | warn("waitpid"); 372 | break; 373 | } 374 | child_pid = -1; 375 | if (WIFEXITED(st) && WEXITSTATUS(st)) { 376 | printf("Exit status: %d\n", WEXITSTATUS(st)); 377 | } 378 | if (WIFSIGNALED(st)) { 379 | printf("Exit signal: %s\n", strsignal(WTERMSIG(st))); 380 | } 381 | break; 382 | } 383 | // flush discards data. 384 | //ok = tcflush(fd, TCIOFLUSH); 385 | ok = tcdrain(fd); 386 | } 387 | 388 | close(fd); 389 | return 0; 390 | } 391 | -------------------------------------------------------------------------------- /pty_shell/pty_shell.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /python/listmedia.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export DYLD_FALLBACK_FRAMEWORK_PATH=../embedded 4 | 5 | ../embedded/mame64 $* -listmedia 6 | 7 | -------------------------------------------------------------------------------- /python/listslots.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export DYLD_FALLBACK_FRAMEWORK_PATH=../embedded 4 | 5 | ../embedded/mame64 $* -listslots 6 | 7 | -------------------------------------------------------------------------------- /python/listxml.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export DYLD_FALLBACK_FRAMEWORK_PATH=../embedded 4 | 5 | for machine in $* ; do ../embedded/mame64 "$machine" -listxml -nodtd ; done 6 | 7 | -------------------------------------------------------------------------------- /python/machines.py: -------------------------------------------------------------------------------- 1 | MACHINES = ( 2 | "apple1", 3 | "apple2", "apple2p", "apple2jp", 4 | "apple3", 5 | 6 | "apple2e", "apple2ede", "apple2efr", "apple2ese", "apple2euk", 7 | "apple2ee", "apple2eede", "apple2eefr", "apple2ees", "apple2eese", "apple2eeuk", 8 | "apple2ep", "apple2epde", "apple2epfr", "apple2epse", "apple2epuk", 9 | 10 | "apple2gs", "apple2gsr0", "apple2gsr1", "apple2gsmt", 11 | "apple2c", "apple2c0", "apple2c3", "apple2c4", "apple2cp", 12 | 13 | # laser family 14 | "laser128", "laser2c", "las128ex", "las128e2", "laser128o", 15 | 16 | # Franklin 17 | "ace100", "ace500", "ace1000", "ace2200", 18 | 19 | # IIe clones 20 | "mprof3", "prav8c", "spectred", "tk3000", 21 | # II clones 22 | "agat7", "agat9", "albert", 23 | "am100", "am64", "basis108", "craft2p", 24 | "dodo", "elppa", "hkc8800a", "ivelultr", 25 | "maxxi", "microeng", "prav82", "prav8m", 26 | "space84", "uniap2en", "uniap2pt", "uniap2ti", 27 | "zijini", 28 | # China Education Computer 29 | "cec2000", "cece", "cecg", "ceci", "cecm", 30 | "las3000", 31 | 32 | 33 | # macintosh... 34 | "macii", "maciihmu", "mac2fdhd", "maciix", "maciicx", "maciici", "maciisi", 35 | "maciivx", "maciivi", "maciifx", 36 | "maclc", "maclc2", "maclc3", "maclc3p", "maclc520", "maclc550", "mactv", 37 | 38 | # mac 128k-classic 39 | "mac128k", "mac512k", "mac512ke", "macplus", "macse", "macsefd", "macse30", 40 | "macclasc", "macclas2", "maccclas", 41 | 42 | # quadra 43 | "macqd700", "macqd800", "macct610", "macct650", "macqd610", "macqd650", 44 | "macqd605", "maclc475", "maclc575", "macqd630", "maclc580", "macqd900", "macqd950", 45 | 46 | # portable 47 | "macprtb", "macpb100", 48 | # powerbook 49 | "macpb140", "macpb145", "macpb145b", "macpb160", "macpb165", "macpb165c", "macpb170", "macpb180", "macpb180c", 50 | "macpd210", "macpd230", "macpd250", "macpd270c", "macpd280", "macpd280c", 51 | 52 | 53 | # 128k clones 54 | # "unitron", "utrn1024", 55 | 56 | 57 | # acorn 58 | "bbcb", "bbca", "bbcb_de", "bbcb_us", "bbcb_no", "bbcbp", "bbcbp128", "bbcm", "bbcmt", "bbcmc", "electron", 59 | 60 | #atari 61 | "st", "megast", 62 | 63 | 64 | # commodore 65 | "c64", "c64c", 66 | 67 | # oric 68 | "oric1", "orica", "prav8d", "telstrat", 69 | # mt65, micron, mt6809 -- need tanbus support... 70 | 71 | 72 | # trs 73 | "coco", "coco2b", "coco3", "coco3p", "mc10", 74 | "cocoh", "coco3h", "coco2bh", 75 | "trs80", "trs80l2", 76 | "dragon32", "dragon64", "d64plus", "dragon200", "dragon200e", "tanodr64", 77 | 78 | 79 | 80 | 81 | 82 | ) 83 | 84 | 85 | MACHINES_EXTRA = MACHINES + ( 86 | 87 | # other (for Ample-lite...) 88 | 89 | # commodore 90 | "c128", 91 | 92 | # amiga 93 | "a500", "a500n", "a1000", "a1000n", "a2000", "a2000n", 94 | 95 | 96 | # DEC 97 | "vt52", "vt100", "vt101", "vt102", "vt240", 98 | "ds2100", "ds3100", "ds5k133", "pdp11qb", "pdp11ub", "pdp11ub2", 99 | # IBM 100 | "rtpc010", "rtpc015", "rtpc020", "rtpc025", "rtpca25", 101 | # HP 102 | "hp9k310", "hp9k320", "hp9k330", "hp9k332", "hp9k340", "hp9k360", "hp9k370", "hp9k380", "hp9k382", 103 | # Intergraph 104 | "ip2000", "ip2400", "ip2500", "ip2700", "ip2800", "ip6000", "ip6400", "ip6700", "ip6800", 105 | # MIPS 106 | "rc2030", "rs2030", "rc3230", "rs3230", 107 | # SGI 108 | "indigo", "indigo2_4415", "indigo_r4000", "indigo_r4400", "indy_4610", "indy_4613", "indy_5015", "pi4d20", "pi4d25", "pi4d30", "pi4d35", 109 | # Sony 110 | "nws3260", "nws3410", "nws1580", "nws5000x", 111 | # SUN 112 | "sun1", "sun2_50", "sun2_120", "sun3_50", "sun3_60", "sun3_110", "sun3_150", "sun3_260", "sun3_e", "sun3_80", "sun4_40", "sun4_50", "sun4_20", "sun4_25", "sun4_65", 113 | # "sun3_460", "sun4_400", "sun4_110", "sun4_300", "sun4_60", "sun4_75", "sun_s10", "sun_s20" 114 | 115 | ) 116 | 117 | 118 | SLOTS = ( 119 | "fdc", # bbc fdc 120 | "sl0", "sl1", "sl2", "sl3", 121 | "sl4", "sl5", "sl6", "sl7", 122 | "exp", "aux", 123 | "rs232", 124 | "gameio", 125 | "printer", 126 | "modem", 127 | 128 | # mac nubus 129 | "nb1", "nb2", "nb3", "nb4", "nb5", "nb6", "nb7", 130 | "nb8", "nb9", "nba", "nbb", "nbc", "nbd", "nbe", 131 | 132 | "pds", "pds030", "lcpds", 133 | 134 | # st 135 | "centronics", "mdin", "mdout", 136 | 137 | # amiga 138 | "zorro1", "zorro2", "zorro3", "zorro4", "zorro5", 139 | 140 | # dec 141 | "eia", "host", "com_port", "prt_port", 142 | "rs232a", "rs232b", "serial0", "serial1", "tty0", "tty1", 143 | "kbd", "mse", "keyboard", "kbd_con", "mouseport", 144 | 145 | "isa0", "isa1", "isa2", "isa3", "isa4", "isa5", "isa6", "isa7", "isa8", "isa9", 146 | "qbus:1", "qbus:2", "qbus:3", "qbus:4", "qbus:5", 147 | 148 | # bbc 149 | "rs423", "tube", "econet254", "analogue", "userport", "internal", "1mhzbus", 150 | 151 | # coco/trs 152 | "ext", "floppy0", "floppy1", "floppy2", "floppy3", 153 | 154 | # commodore 155 | "user", "iec4", "iec8", "iec9", "iec10", "iec11", "tape" 156 | ) 157 | 158 | SLOT_NAMES = { 159 | "ramsize": "RAM", 160 | "bios": "ROM", 161 | "sl0": "Slot 0", 162 | "sl1": "Slot 1", 163 | "sl2": "Slot 2", 164 | "sl3": "Slot 3", 165 | "sl4": "Slot 4", 166 | "sl5": "Slot 5", 167 | "sl6": "Slot 6", 168 | "sl7": "Slot 7", 169 | "exp": "Expansion", 170 | "aux": "Auxiliary", 171 | "rs232": "Serial", 172 | "gameio": "Game I/O", 173 | "modem": "Modem", 174 | "printer": "Printer", 175 | 176 | "nb9": "Slot 9", 177 | "nba": "Slot A", 178 | "nbb": "Slot B", 179 | "nbc": "Slot C", 180 | "nbd": "Slot D", 181 | "nbe": "Slot E", 182 | 183 | "pds": "PDS", 184 | "pds030": "PDS", 185 | "lcpds": "PDS", 186 | 187 | "centronics": "Printer", 188 | "mdin": "MIDI In", 189 | "mdout": "MIDI Out", 190 | 191 | "zorro1": "Zorro 1", 192 | "zorro2": "Zorro 2", 193 | "zorro3": "Zorro 3", 194 | "zorro4": "Zorro 4", 195 | "zorro5": "Zorro 5", 196 | 197 | 198 | "kbd": "Keyboard", 199 | "keyboard": "Keyboard", 200 | "kbd_con": "Keyboard", 201 | "mse": "Mouse", 202 | "mouseport": "Mouse", 203 | 204 | "rs423": "Serial", 205 | "eia": "Serial", 206 | "host": "Serial", 207 | "com_port": "Serial", 208 | "prt_port": "Printer", 209 | "rs232a": "Serial A", 210 | "rs232b": "Serial B", 211 | "serial0": "Serial 0", 212 | "serial1": "Serial 1", 213 | "tty0": "TTY 0", 214 | "tty1": "TTY 1", 215 | 216 | "isa0": "Slot 0", 217 | "isa1": "Slot 1", 218 | "isa2": "Slot 2", 219 | "isa3": "Slot 3", 220 | "isa4": "Slot 4", 221 | "isa5": "Slot 5", 222 | "isa6": "Slot 6", 223 | "isa7": "Slot 7", 224 | "isa8": "Slot 8", 225 | "isa9": "Slot 9", 226 | 227 | "qbus:1": "Q-Bus 1", 228 | "qbus:2": "Q-Bus 2", 229 | "qbus:3": "Q-Bus 3", 230 | "qbus:4": "Q-Bus 4", 231 | "qbus:5": "Q-Bus 5", 232 | 233 | "tube": "Tube", 234 | "econet254": "Econet", 235 | "analogue": "Analog Port", 236 | "userport": "User Port", 237 | "internal": "Internal", 238 | "1mhzbus": "1MHz Bus", 239 | "fdc": "Disk Drives", 240 | 241 | 242 | # "ext": "Coco Cart", 243 | "ext": "Expansion", 244 | "floppy0": "Floppy 1", 245 | "floppy1": "Floppy 2", 246 | "floppy2": "Floppy 3", 247 | "floppy3": "Floppy 4", 248 | 249 | # commodore 250 | "user": "User", 251 | "tape": "Tape", 252 | "iec4": "IEC 4", 253 | "iec5": "IEC 5", 254 | "iec6": "IEC 6", 255 | "iec7": "IEC 7", 256 | "iec8": "IEC 8", 257 | "iec9": "IEC 9", 258 | "iec10": "IEC 10", 259 | "iec11": "IEC 11", 260 | "iec12": "IEC 12", 261 | } 262 | 263 | 264 | -------------------------------------------------------------------------------- /python/mame.py: -------------------------------------------------------------------------------- 1 | 2 | import subprocess 3 | 4 | 5 | def run(*args): 6 | 7 | env = {'DYLD_FALLBACK_FRAMEWORK_PATH': '../embedded'} 8 | path = "../embedded/mame64" 9 | path = "../mame/mame-x64" 10 | 11 | st = subprocess.run([path, *args], capture_output=True, env=env, text=True, check=True) 12 | 13 | #if st.returncode != 0: 14 | # print("mame error: {}".format(m)) 15 | # exit(1) 16 | 17 | return st.stdout 18 | -------------------------------------------------------------------------------- /python/mkdevices.py: -------------------------------------------------------------------------------- 1 | 2 | import subprocess 3 | 4 | from plist import to_plist 5 | 6 | import xml.etree.ElementTree as ET 7 | 8 | from machines import MACHINES 9 | 10 | 11 | devices = {} 12 | 13 | for m in MACHINES: 14 | 15 | 16 | st = subprocess.run(["mame", m, "-listxml"], capture_output=True) 17 | if st.returncode != 0: 18 | print("mame error: {}".format(m)) 19 | exit(1) 20 | 21 | xml = st.stdout 22 | root = ET.fromstring(xml) 23 | 24 | nodes = root.findall("machine[@isdevice='yes']") 25 | for d in nodes: 26 | 27 | name = d.get("name") # devname 28 | desc = d.find("description").text 29 | 30 | tmp = { 31 | "Name": name, 32 | "Description": desc 33 | } 34 | devices[name] = tmp 35 | 36 | 37 | with open("../Ample/Resources/devices.plist", "w") as f: 38 | f.write(to_plist(devices)) 39 | -------------------------------------------------------------------------------- /python/mkmodels.py: -------------------------------------------------------------------------------- 1 | 2 | import xml.etree.ElementTree as ET 3 | import re 4 | import sys 5 | import argparse 6 | 7 | from plist import to_plist 8 | from machines import MACHINES, MACHINES_EXTRA 9 | import mame 10 | 11 | 12 | apple1_children = None 13 | apple2_children = ["apple2", "apple2p", "apple2jp"] 14 | apple3_children = None 15 | apple2e_children = ["apple2e", "apple2euk", "apple2ede", "apple2ese", "apple2efr", "apple2ees"] 16 | apple2ee_children = ["apple2ee", "apple2eeuk", "apple2eede", "apple2eese", "apple2eefr"] 17 | apple2ep_children = ["apple2ep", "apple2epuk", "apple2epde", "apple2epse", "apple2epfr"] 18 | 19 | apple2c_children = ["apple2c", "apple2c0", "apple2c3", "apple2c4", "apple2cp"] 20 | apple2gs_children = ["apple2gsr0", "apple2gsr1", "apple2gs"] 21 | laser_children = ["las3000", "laser2c", "laser128", "laser128o", "las128ex", "las128e2"] 22 | franklin_children = ["ace100", "ace500", "ace1000", "ace2200"] 23 | ii_clones_children = ["albert", 24 | "am100", "am64", "basis108", "craft2p", 25 | "dodo", "elppa", "hkc8800a", "ivelultr", 26 | "maxxi", "microeng", "prav82", "prav8m", 27 | "space84", "uniap2en", "uniap2pt", "uniap2ti"] 28 | iie_clones_children = ["mprof3", "prav8c", "spectred", "tk3000", "zijini"] 29 | cec_children = ["cec2000", "cece", "cecg", "ceci", "cecm"] 30 | agat_children = ["agat7", "agat9"] 31 | 32 | mac_ii_children = [ 33 | "macii", "maciihmu", "mac2fdhd", "maciix", "maciifx", "maciicx", "maciici", "maciisi", "maciivx", "maciivi", 34 | 35 | ] 36 | 37 | mac_lc_children = [ 38 | "maclc", "maclc2", "maclc3", "maclc3p", 39 | "maclc475", "maclc520", "maclc550", "maclc575", 40 | "macct610", "macct650", "mactv", 41 | ] 42 | # maclc50" / macqd630 slots are messed up right now. 43 | 44 | mac_quadra_children = [ 45 | "macqd605", "macqd610", "macqd650", "macqd700", "macqd800", "macqd900", "macqd950" 46 | ] 47 | 48 | # se/30 and classic 2 are implemented as a nubus but i'm sticking then with the 128 due to the form factor. 49 | mac_128k_children = ["mac128k", "mac512k", "mac512ke", "macplus", 50 | "macse", "macsefd", "macse30", "macclasc", "macclas2", "maccclas"] 51 | 52 | 53 | mac_portable_children = ["macprtb", "macpb100"] 54 | mac_powerbook_children = [ 55 | "macpb140", "macpb145", "macpb145b", "macpb160", "macpb165", "macpb165c", "macpb170", "macpb180", "macpb180c", 56 | "macpd210", "macpd230", "macpd250", "macpd270c", "macpd280", "macpd280c" 57 | ] 58 | 59 | atari_st_children = ["st", "megast"] 60 | 61 | tandy_children = [ 62 | "trs80", "trs80l2", 63 | "coco", "cocoh", 64 | "coco2b", "coco2bh", 65 | "coco3", "coco3p", "coco3h", 66 | "mc10", 67 | "dragon32", "dragon64", "d64plus", "dragon200", "dragon200e", "tanodr64", 68 | ] 69 | 70 | oric_children = [ 71 | "oric1", "orica", "prav8d", "telstrat", 72 | ] 73 | 74 | 75 | amiga_children = ["a500", "a500n", "a1000", "a1000n", "a2000", "a2000n" ] 76 | 77 | acorn_children = [ "bbcb", "bbca", "bbcb_de", "bbcb_us", "bbcb_no", "bbcbp", "bbcbp128", "bbcm", "bbcmt", "bbcmc", "electron" ] 78 | commodore_children = ["c64", "c64c"] 79 | commodore_children_extra = ["c64", "c64c", "c128"] 80 | 81 | dec_vt_children = ["vt52", "vt100", "vt101", "vt102", "vt240"] 82 | dec_children = ["ds2100", "ds3100", "ds5k133", "pdp11qb", "pdp11ub", "pdp11ub2"] 83 | ibm_rt_children = ["rtpc010", "rtpc015", "rtpc020", "rtpc025", "rtpca25"] 84 | hp_9000_children = ["hp9k310", "hp9k320", "hp9k330", "hp9k332", "hp9k340", "hp9k360", "hp9k370", "hp9k380", "hp9k382"] 85 | intergraph_children = ["ip2000", "ip2400", "ip2500", "ip2700", "ip2800", "ip6000", "ip6400", "ip6700", "ip6800"] 86 | mips_children = ["rc2030", "rs2030", "rc3230", "rs3230"] 87 | sgi_children = ["indigo", "indigo2_4415", "indigo_r4000", "indigo_r4400", "indy_4610", "indy_4613", "indy_5015", "pi4d20", "pi4d25", "pi4d30", "pi4d35"] 88 | sony_children = ["nws3260", "nws3410", "nws1580", "nws5000x"] 89 | sun_children = [ 90 | "sun1", "sun2_50", "sun2_120", "sun3_50", "sun3_60", "sun3_110", "sun3_150", "sun3_260", "sun3_e", "sun3_80", 91 | "sun4_40", "sun4_50", "sun4_20", "sun4_25", "sun4_65", 92 | ] 93 | 94 | TREE = [ 95 | ("Apple I", "apple1", apple1_children), 96 | ("Apple ][", "apple2", apple2_children), 97 | ("Apple IIe", "apple2e", apple2e_children), 98 | ("Apple IIe (enhanced)", "apple2ee", apple2ee_children), 99 | ("Apple IIe (platinum)", "apple2p", apple2ep_children), 100 | ("Apple //c", "apple2c", apple2c_children), 101 | ("Apple IIgs", "apple2gs", apple2gs_children), 102 | ("Apple ///", "apple3", apple3_children), 103 | ("II Clones", None, ii_clones_children), 104 | ("IIe Clones", None, iie_clones_children), 105 | ("Franklin", None, franklin_children), 106 | ("Laser", "laser128", laser_children), 107 | ("Agat", "agat7", agat_children), 108 | ("China Education Computer", None, cec_children), 109 | ("Macintosh (Compact)", "macse30", mac_128k_children), 110 | ("Macintosh (II)", "maciix", mac_ii_children), 111 | ("Macintosh (Quadra)", None, mac_quadra_children), 112 | ("Macintosh (LC)", None, mac_lc_children), 113 | ("Macintosh (Portable)", None, mac_portable_children), 114 | ("Macintosh (PowerBook)", None, mac_powerbook_children), 115 | ("Acorn", None, acorn_children), 116 | ("Atari ST", "st", atari_st_children), 117 | ("Commodore", "c64", commodore_children), 118 | ("Oric", None, oric_children), 119 | ("Tandy", None, tandy_children), 120 | ] 121 | 122 | TREE_EXTRA = TREE + [ 123 | ("Amiga", None, amiga_children), 124 | ("DEC VT", None, dec_vt_children), 125 | ("DEC", None, dec_children), 126 | ("HP 9000", None, hp_9000_children), 127 | ("IBM RT", None, ibm_rt_children), 128 | ("Intergraph", None, intergraph_children), 129 | ("MIPS", None, mips_children), 130 | ("SGI", None, sgi_children), 131 | ("Sony", None, sony_children), 132 | ("SUN", None, sun_children), 133 | ] 134 | 135 | 136 | 137 | extra = False 138 | machines = MACHINES 139 | tree = TREE 140 | 141 | 142 | p = argparse.ArgumentParser() 143 | p.add_argument('--extra', action='store_true') 144 | p.add_argument('machine', nargs="*") 145 | args = p.parse_args() 146 | 147 | extra = args.extra 148 | 149 | if extra: 150 | machines = MACHINES_EXTRA 151 | tree = TREE_EXTRA 152 | 153 | 154 | # Name: Description: 155 | # apple2gs "Apple IIgs (ROM03)" 156 | # apple2gsr0 "Apple IIgs (ROM00)" 157 | 158 | names = {} 159 | 160 | #t = st.stdout 161 | 162 | t = mame.run("-listfull", *machines) 163 | 164 | lines = t.split("\n") 165 | lines.pop(0) 166 | for x in lines: 167 | x = x.strip() 168 | if x == "": continue 169 | m = re.fullmatch(r"^([A-Za-z0-9_]+)\s+\"([^\"]+)\"$", x) 170 | if not m: 171 | print("hmmm....", x) 172 | continue 173 | name = m[1] 174 | desc = m[2] 175 | 176 | names[name] = desc 177 | 178 | 179 | def make_children(clist): 180 | global names 181 | return [ 182 | { "description": names[x], "value": x} 183 | for x in clist 184 | ] 185 | 186 | data = [] 187 | 188 | 189 | for x in tree: 190 | desc, value, children = x 191 | tmp = { "description": desc } 192 | if value: tmp["value"] = value 193 | if children: tmp["children"] = make_children(children) 194 | 195 | data.append(tmp) 196 | 197 | if extra: 198 | path = "../Ample/Resources/models~extra.plist" 199 | else: 200 | path = "../Ample/Resources/models.plist" 201 | with open(path, "w") as f: 202 | f.write(to_plist(data)) 203 | 204 | -------------------------------------------------------------------------------- /python/mkroms.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import re 3 | 4 | import xml.etree.ElementTree as ET 5 | from html.parser import HTMLParser 6 | from os.path import splitext 7 | 8 | 9 | from machines import MACHINES, MACHINES_EXTRA 10 | import mame 11 | from plist import to_plist 12 | 13 | 14 | # a2pcxport dependencies. not automatically included though 15 | # (would need to manually pull devnames from a2pcxport then) 16 | # listxml for them. 17 | 18 | EXTRA_MACHINES = [ 19 | 'vgmplay', 20 | 'cga', 21 | 'kb_iskr1030', 22 | 'kb_ec1841', 23 | 'kb_pc83', 24 | 'kb_pcxt83', 25 | 'keytronic_pc3270', 26 | 'apple2gsr0p', 27 | 'apple2gsr0p2', 28 | 'apple2c0', 29 | 'apple2c3', 30 | 'apple2c4', 31 | 'mac2fdhd', 32 | 'cuda', 33 | ] 34 | 35 | 36 | p = argparse.ArgumentParser() 37 | p.add_argument('--full', action='store_true') 38 | p.add_argument('--extra', action='store_true') 39 | p.add_argument('machine', nargs="*") 40 | args = p.parse_args() 41 | 42 | # full = args.full 43 | extra = args.extra 44 | machines = args.machine 45 | if not machines: 46 | if extra: 47 | machines = [ *MACHINES_EXTRA, *EXTRA_MACHINES] 48 | else: 49 | machines = [ *MACHINES, *EXTRA_MACHINES] 50 | 51 | # roms stored in other files. 52 | xEXCLUDE = [ 53 | 'ace100', 54 | 'agat7', 55 | 'agat9', 56 | 'albert', 57 | 'am100', 58 | 'am64', 59 | 'apple2cp', 60 | 'apple2ee', 61 | 'apple2eefr', 62 | 'apple2ees', 63 | 'apple2eeuk', 64 | 'apple2ep', 65 | 'apple2euk', 66 | 'apple2gsr0', 67 | 'apple2gsr1', 68 | 'apple2jp', 69 | 'apple2p', 70 | 'basis108', 71 | 'craft2p', 72 | 'dodo', 73 | 'elppa', 74 | 'hkc8800a', 75 | 'ivelultr', 76 | 'las128e2', 77 | 'las128ex', 78 | 'laser128', 79 | 'laser2c', 80 | 'maxxi', 81 | 'microeng', 82 | 'mprof3', 83 | 'prav82', 84 | 'prav8c', 85 | 'prav8m', 86 | 'space84', 87 | 'spectred', 88 | 'uniap2en', 89 | 'uniap2pt', 90 | 'uniap2ti', 91 | ] 92 | 93 | # non-existent or included elsewhere. 94 | EXCLUDE = set([ 95 | 'mac512ke', 96 | 'maciicx', 97 | 'maciihmu', 98 | 'maciivi', 99 | 'maciix', 100 | 101 | 'macct610', 102 | 'macct650', 103 | 'maclc3p', 104 | 'maclc475', 105 | 'maclc575', 106 | 'macqd610', 107 | 'macqd650', 108 | 109 | 'kb_pc83', 110 | 111 | # ROMs for CD Drives, etc, that are intentionally hidden 112 | # due to lack of functionality. 113 | 'aplcd150', 114 | 'cdd2000', 115 | 'cdr4210', 116 | 'cdrn820s', 117 | 'cdu415', 118 | 'cdu561_25', 119 | 'cdu75s', 120 | 'cfp1080s', 121 | 'crd254sh', 122 | 'cw7501', 123 | 'smoc501', 124 | 125 | # amiga ntsc 126 | "a500n", 127 | "a1000n", 128 | "a2000n", 129 | ]) 130 | 131 | def fix_machine_description(x, devname): 132 | # CFFA 2.0 Compact Flash (65C02 firmware, www.dreher.net) 133 | x = x.replace(", www.dreher.net","") 134 | x = x.replace('8inch','8"') # 135 | x = x.replace("65C02", "65c02") 136 | x = re.sub(r"((^| |\()[a-z])", lambda x: x[0].upper(), x) # fix capital-case 137 | 138 | if devname in ("st", "megast"): x = "Atari " + x 139 | return x 140 | 141 | def build_known_roms_list(): 142 | # https://archive.org/download/mame-merged/mame-merged/ 143 | infile = "mame-0.273-merged.html" 144 | # infile = "mame-0233-full.html" 145 | # infile = "mame-0.231-merged.html" 146 | rv = set() 147 | 148 | class X(HTMLParser): 149 | rv = set() 150 | 151 | def handle_starttag(self, tag, attrs): 152 | if tag != 'a': return 153 | href = None 154 | for xx in attrs: 155 | if xx[0] == 'href': 156 | href = xx[1] 157 | break 158 | if not href: return 159 | root, ext = splitext(href) 160 | if ext in (".7z", ".zip"): self.rv.add(root) 161 | 162 | 163 | 164 | x = X() 165 | with open(infile) as f: 166 | data = f.read() 167 | x.feed(data) 168 | x.close() 169 | return x.rv 170 | 171 | 172 | 173 | 174 | mnames = {} 175 | rnames = set() 176 | 177 | known = build_known_roms_list() 178 | 179 | known.add('macpb180c') 180 | known.add('macpd210') 181 | known.add('macpd270c') 182 | known.add('macpd280c') 183 | known.add('m68hc05pge') 184 | 185 | 186 | for m in machines: 187 | 188 | print(m) 189 | 190 | xml = mame.run(m, "-listxml") 191 | root = ET.fromstring(xml) 192 | 193 | data = { } 194 | 195 | 196 | # find machines that have a rom child 197 | for x in root.findall('machine/rom/..'): 198 | name = x.get('name') 199 | #if name in EXCLUDE: continue 200 | if name in mnames: continue 201 | 202 | if name not in known: continue 203 | # if name not in known: 204 | # print("skipping", name) 205 | # continue 206 | 207 | mnames[name] = x.find("description").text 208 | 209 | 210 | #if (name in known): mnames.add(name) 211 | # if name in mnames: 212 | # mnames[name].append(m) 213 | # else: 214 | # mnames[name] = [ m ] 215 | # mnames.add(name) 216 | # for y in x.findall('./rom'): 217 | # rnames.add(y.get('name')) 218 | 219 | 220 | # print("\n\n\n") 221 | # ll = list(mnames.difference(EXCLUDE)) 222 | # ll.sort() 223 | # for x in ll: 224 | # print(x) 225 | 226 | # if full: ROMS = list(mnames) 227 | # else: ROMS = list(mnames.difference(EXCLUDE)) 228 | ROMS = [{ 'value': k, 'description': fix_machine_description(v, k)} for k, v in mnames.items()]; 229 | ROMS.sort(key=lambda x: x.get('description')) 230 | 231 | # data = [] 232 | # data["source"] = "https://archive.org/download/mame0.224" 233 | # data["type"] = "zip" 234 | # data["version"] = "0.224" 235 | #data["roms"] = ROMS 236 | 237 | # for k in ROMS: 238 | # data.append( { 'value': k, 'description': mnames[k] }) 239 | 240 | 241 | # print(ROMS) 242 | if extra: 243 | path = "../Ample/Resources/roms~extra.plist" 244 | else: 245 | path = "../Ample/Resources/roms.plist" 246 | 247 | with open(path, "w") as f: 248 | f.write(to_plist(ROMS)) 249 | -------------------------------------------------------------------------------- /python/plist.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | __all__ = ['to_plist'] 4 | 5 | from xml.sax.saxutils import escape 6 | from base64 import b64encode 7 | from datetime import date, datetime, timezone 8 | 9 | _header = ( 10 | '\n' 11 | '\n' 12 | '\n' 13 | ) 14 | _trailer = '\n' 15 | 16 | INDENT = " " 17 | 18 | def _bad(x, akku, indent=""): 19 | raise ValueError("plist: bad type: {} ({})".format(type(x), x)) 20 | 21 | 22 | 23 | def _encode_array(x, akku, indent=""): 24 | 25 | indent2 = indent + INDENT 26 | 27 | akku.append(indent + "\n") 28 | 29 | for v in x: 30 | _encoder.get(type(v), _bad)(v, akku, indent2) 31 | 32 | akku.append(indent + "\n") 33 | 34 | 35 | def _encode_dict(x, akku, indent=""): 36 | 37 | indent2 = indent + INDENT 38 | 39 | akku.append(indent + "\n") 40 | for k,v in x.items(): 41 | # key must be string? 42 | if type(k) != str: 43 | raise ValueError("plist: dictionary key must be string: {}: {}".format(type(k), k)) 44 | akku.append("{}{}\n".format(indent2, escape(k))) 45 | _encoder.get(type(v), _bad)(v, akku, indent2) 46 | 47 | akku.append(indent + "\n") 48 | 49 | 50 | def _encode_bool(x, akku, indent=""): 51 | if x: akku.append(indent + "\n") 52 | else: akku.append(indent + "\n") 53 | 54 | def _encode_integer(x, akku, indent=""): 55 | akku.append("{}{}\n".format(indent, x)) 56 | 57 | def _encode_real(x, akku, indent=""): 58 | akku.append("{}{}\n".format(indent, x)) 59 | 60 | def _encode_string(x, akku, indent=""): 61 | akku.append("{}{}\n".format(indent, escape(x))) 62 | 63 | 64 | # data is YYYY-MM-DD T HH:MM:SS Z 65 | def _encode_date(x, akku, indent=""): 66 | s = x.strftime('%Y-%m-%d') 67 | akku.append("{}{}\n".format(indent, s)) 68 | 69 | def _encode_datetime(x, akku, indent=""): 70 | # if not x.tzinfo 71 | # raise ValueError("plist: datetime must have tzinfo: {}".format(x)) 72 | 73 | # if x.tzinfo.utcoffset(x) == None: 74 | # raise ValueError("plist: datetime must have utc offset: {}".format(x)) 75 | 76 | utc = x.astimezone(timezone.utc) 77 | s = utc.strftime('%Y-%m-%dT%H:%M:%SZ') 78 | akku.append("{}{}\n".format(indent, s)) 79 | 80 | def _encode_data(x, akku, indent=""): 81 | # data is base64 encoded 82 | 83 | CHUNKSIZE = 32 84 | if len(x) < CHUNKSIZE: 85 | akku.append("{}{}\n".format(indent, b64encode(x).encode('ascii'))) 86 | return 87 | 88 | indent2 = indent + INDENT 89 | akku.append(indent + "\n") 90 | 91 | for i in range(0, len(x), CHUNKSIZE): 92 | akku.append( 93 | "{}{}\n".format( 94 | indent2, 95 | b64encode(x[i:i+CHUNKSIZE]).encode('ascii') 96 | ) 97 | ) 98 | 99 | akku.append(indent + "\n") 100 | 101 | # data, data not yet supported. 102 | _encoder = { 103 | str: _encode_string, 104 | float: _encode_real, 105 | int: _encode_integer, 106 | bool: _encode_bool, 107 | 108 | tuple: _encode_array, 109 | list: _encode_array, 110 | dict: _encode_dict, 111 | bytes: _encode_data, 112 | bytearray: _encode_data, 113 | date: _encode_date, 114 | datetime: _encode_datetime, 115 | } 116 | 117 | def to_plist(x): 118 | 119 | akku = [] 120 | akku.append(_header) 121 | _encoder.get(type(x), _bad)(x, akku, INDENT) 122 | akku.append(_trailer) 123 | 124 | return ''.join(akku) 125 | 126 | 127 | -------------------------------------------------------------------------------- /python/rom.py: -------------------------------------------------------------------------------- 1 | from plist import to_plist 2 | 3 | ROMS = """ 4 | a1cass 5 | a2aevm80 6 | a2ap16 7 | a2ap16a 8 | a2aplcrd 9 | a2cffa02 10 | a2cffa2 11 | a2corvus 12 | a2diskii 13 | a2diskiing 14 | a2focdrv 15 | a2hsscsi 16 | a2iwm 17 | a2memexp 18 | a2mouse 19 | a2pic 20 | a2ramfac 21 | a2scsi 22 | a2ssc 23 | a2surance 24 | a2swyft 25 | a2thunpl 26 | a2tmstho 27 | a2twarp 28 | a2ultrme 29 | a2ulttrm 30 | a2vidtrm 31 | a2vtc1 32 | a2vulcan 33 | a2vulgld 34 | a2vuliie 35 | a2zipdrv 36 | a3fdc 37 | apple1 38 | apple2 39 | apple2c 40 | apple2e 41 | apple2gs 42 | apple3 43 | cec2000 44 | cece 45 | cecg 46 | ceci 47 | cecm 48 | cga 49 | cmsscsi 50 | d2fdc 51 | diskii13 52 | keytronic_pc3270 53 | m68705p3 54 | votrax 55 | zijini 56 | agat_fdc 57 | a2sider1 58 | a2sider2 59 | qsound 60 | ym2608 61 | 62 | a2grapplerplus 63 | a2pic 64 | 65 | # .227 66 | a2parprn 67 | a2suprterm 68 | a2uniprint 69 | ccs7710 70 | 71 | #.228 72 | aprissi 73 | 74 | # macintosh 75 | maclc 76 | maclc2 77 | maclc3 78 | maciici 79 | egret 80 | nb_image 81 | nb_824gc 82 | nb_aenet 83 | nb_qdlink 84 | nb_amc3b 85 | nb_btbug 86 | nb_m2hr 87 | nb_wspt 88 | nb_m2vc 89 | nb_vikbw 90 | nb_rtpd 91 | nb_c264 92 | nb_laserview 93 | nb_spdq 94 | nb_sp8s3 95 | """.splitlines() 96 | 97 | # 98 | # others 99 | # mprof3 100 | # spectred 101 | # tk3000 102 | # prav8c 103 | # 104 | 105 | ROMS = [x for x in ROMS if x != ""] 106 | ROMS = [x for x in ROMS if x[0] != "#"] 107 | ROMS.sort() 108 | 109 | data = {} 110 | data["source"] = "https://archive.org/download/mame0.224" 111 | data["type"] = "zip" 112 | data["version"] = "0.224" 113 | data["roms"] = ROMS 114 | 115 | # print(ROMS) 116 | with open("../Ample/Resources/roms.plist", "w") as f: 117 | f.write(to_plist(data)) 118 | -------------------------------------------------------------------------------- /screenshots/2020-08-30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ksherlock/ample/60cb69df42b436de43bcaa87a895c28212ee9757/screenshots/2020-08-30.png -------------------------------------------------------------------------------- /screenshots/2020-09-06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ksherlock/ample/60cb69df42b436de43bcaa87a895c28212ee9757/screenshots/2020-09-06.png -------------------------------------------------------------------------------- /screenshots/2020-09-14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ksherlock/ample/60cb69df42b436de43bcaa87a895c28212ee9757/screenshots/2020-09-14.png -------------------------------------------------------------------------------- /screenshots/2021-07-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ksherlock/ample/60cb69df42b436de43bcaa87a895c28212ee9757/screenshots/2021-07-01.png -------------------------------------------------------------------------------- /screenshots/2021-07-01@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ksherlock/ample/60cb69df42b436de43bcaa87a895c28212ee9757/screenshots/2021-07-01@2x.png -------------------------------------------------------------------------------- /vmnet_helper/vmnet_helper.c: -------------------------------------------------------------------------------- 1 | /* vmnet helper */ 2 | /* because it needs root permissions ... sigh */ 3 | 4 | /* 5 | * basicly... run as root, read messages from stdin, write to stdout. 6 | */ 7 | 8 | 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | 22 | static interface_ref interface; 23 | static uint8_t interface_mac[6]; 24 | static long interface_mtu; 25 | static long interface_packet_size; 26 | static vmnet_return_t interface_status; 27 | 28 | static size_t buffer_size = 0; 29 | static uint8_t *buffer = NULL; 30 | 31 | enum { 32 | MSG_QUIT, 33 | MSG_STATUS, 34 | MSG_READ, 35 | MSG_WRITE 36 | }; 37 | #define MAKE_MSG(msg, extra) (msg | ((extra) << 8)) 38 | 39 | ssize_t safe_read(void *buffer, size_t nbyte) { 40 | 41 | ssize_t rv; 42 | for(;;) { 43 | rv = read(STDIN_FILENO, buffer, nbyte); 44 | if (rv < 0) { 45 | if (errno == EINTR) continue; 46 | err(1, "read"); 47 | } 48 | break; 49 | } 50 | return rv; 51 | } 52 | 53 | 54 | ssize_t safe_readv(const struct iovec *iov, int iovcnt) { 55 | 56 | ssize_t rv; 57 | for(;;) { 58 | rv = readv(STDIN_FILENO, iov, iovcnt); 59 | if (rv < 0) { 60 | if (errno == EINTR) continue; 61 | err(1, "readv"); 62 | } 63 | break; 64 | } 65 | return rv; 66 | } 67 | 68 | ssize_t safe_write(const void *buffer, size_t nbyte) { 69 | 70 | ssize_t rv; 71 | for(;;) { 72 | rv = write(STDOUT_FILENO, buffer, nbyte); 73 | if (rv < 0) { 74 | if (errno == EINTR) continue; 75 | err(1, "write"); 76 | } 77 | break; 78 | } 79 | return rv; 80 | } 81 | 82 | ssize_t safe_writev(const struct iovec *iov, int iovcnt) { 83 | 84 | ssize_t rv; 85 | for(;;) { 86 | rv = writev(STDOUT_FILENO, iov, iovcnt); 87 | if (rv < 0) { 88 | if (errno == EINTR) continue; 89 | err(1, "writev"); 90 | } 91 | break; 92 | } 93 | return rv; 94 | } 95 | 96 | 97 | void msg_status(uint32_t size) { 98 | struct iovec iov[4]; 99 | 100 | uint32_t msg = MAKE_MSG(MSG_STATUS, 6 + 4 + 4); 101 | 102 | iov[0].iov_len = 4; 103 | iov[0].iov_base = &msg; 104 | 105 | iov[1].iov_len = 6; 106 | iov[1].iov_base = interface_mac; 107 | 108 | iov[2].iov_len = 4; 109 | iov[2].iov_base = &interface_mtu; 110 | 111 | iov[3].iov_len = 4; 112 | iov[3].iov_base = &interface_packet_size; 113 | 114 | 115 | safe_writev(iov, 4); 116 | } 117 | 118 | int classify_mac(uint8_t *mac) { 119 | if ((mac[0] & 0x01) == 0) return 1; /* unicast */ 120 | if (memcmp(mac, "\xff\xff\xff\xff\xff\xff", 6) == 0) return 0xff; /* broadcast */ 121 | return 2; /* multicast */ 122 | } 123 | 124 | void msg_read(uint32_t flags) { 125 | /* flag to block broadcast, multicast, etc? */ 126 | 127 | int count = 1; 128 | int xfer; 129 | vmnet_return_t st; 130 | struct vmpktdesc v; 131 | struct iovec iov[2]; 132 | 133 | uint32_t msg; 134 | 135 | 136 | for(;;) { 137 | int type; 138 | 139 | iov[0].iov_base = buffer; 140 | iov[0].iov_len = interface_packet_size; 141 | 142 | v.vm_pkt_size = interface_packet_size; 143 | v.vm_pkt_iov = iov; 144 | v.vm_pkt_iovcnt = 1; 145 | v.vm_flags = 0; 146 | 147 | count = 1; 148 | st = vmnet_read(interface, &v, &count); 149 | if (st != VMNET_SUCCESS) errx(1, "vmnet_read"); 150 | 151 | if (count < 1) break; 152 | /* todo -- skip multicast messages based on flag? */ 153 | type = classify_mac(buffer); 154 | if (type == 2) continue; /* multicast */ 155 | break; 156 | } 157 | 158 | xfer = count == 1 ? (int)v.vm_pkt_size : 0; 159 | msg = MAKE_MSG(MSG_READ, xfer); 160 | iov[0].iov_len = 4; 161 | iov[0].iov_base = &msg; 162 | iov[1].iov_len = xfer; 163 | iov[1].iov_base = buffer; 164 | 165 | safe_writev(iov, count == 1 ? 2 : 1); 166 | } 167 | 168 | 169 | void msg_write(uint32_t size) { 170 | 171 | ssize_t ok; 172 | 173 | int count = 1; 174 | vmnet_return_t st; 175 | struct vmpktdesc v; 176 | struct iovec iov; 177 | uint32_t msg; 178 | 179 | if (size > interface_packet_size) errx(1, "packet too big"); 180 | for(;;) { 181 | ok = safe_read(buffer, size); 182 | if (ok < 0) err(1,"read"); 183 | if (ok != size) errx(1,"message truncated"); 184 | break; 185 | } 186 | 187 | iov.iov_base = buffer; 188 | iov.iov_len = size; 189 | 190 | v.vm_pkt_size = size; 191 | v.vm_pkt_iov = &iov; 192 | v.vm_pkt_iovcnt = 1; 193 | v.vm_flags = 0; 194 | 195 | st = vmnet_write(interface, &v, &count); 196 | 197 | if (st != VMNET_SUCCESS) errx(1, "vmnet_write"); 198 | 199 | 200 | msg = MAKE_MSG(MSG_WRITE, size); 201 | iov.iov_len = 4; 202 | iov.iov_base = &msg; 203 | 204 | safe_writev(&iov, 1); 205 | } 206 | 207 | /* 208 | * Drop privileges according to the CERT Secure C Coding Standard section 209 | * POS36-C 210 | * https://www.securecoding.cert.org/confluence/display/c/POS36-C.+Observe+correct+revocation+order+while+relinquishing+privileges 211 | */ 212 | static int drop_privileges(void) { 213 | // If we are not effectively root, don't drop privileges 214 | if (geteuid() != 0 && getegid() != 0) { 215 | return 0; 216 | } 217 | if (setgid(getgid()) == -1) { 218 | return -1; 219 | } 220 | if (setuid(getuid()) == -1) { 221 | return -1; 222 | } 223 | return 0; 224 | } 225 | 226 | void vmnet_start_interface_failed(void) { 227 | 228 | warnx("vmnet_start_interface failed"); 229 | if (geteuid() != 0) { 230 | fputs( 231 | "\n\n" 232 | "\tvmnet_helper must be run as root.\n" 233 | "\tGo to Ample -> Preferences and Fix VMNet Permissions.\n\n" 234 | , stderr); 235 | } 236 | exit(1); 237 | } 238 | 239 | void vm_startup(void) { 240 | 241 | xpc_object_t dict; 242 | dispatch_queue_t q; 243 | dispatch_semaphore_t sem; 244 | 245 | 246 | memset(interface_mac, 0, sizeof(interface_mac)); 247 | interface_status = 0; 248 | interface_mtu = 0; 249 | interface_packet_size = 0; 250 | 251 | dict = xpc_dictionary_create(NULL, NULL, 0); 252 | xpc_dictionary_set_uint64(dict, vmnet_operation_mode_key, VMNET_SHARED_MODE); 253 | sem = dispatch_semaphore_create(0); 254 | q = dispatch_get_global_queue(QOS_CLASS_UTILITY, 0); 255 | 256 | interface = vmnet_start_interface(dict, q, ^(vmnet_return_t status, xpc_object_t params){ 257 | interface_status = status; 258 | if (status == VMNET_SUCCESS) { 259 | const char *cp; 260 | cp = xpc_dictionary_get_string(params, vmnet_mac_address_key); 261 | fprintf(stderr, "vmnet mac: %s\n", cp); 262 | sscanf(cp, "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx", 263 | &interface_mac[0], 264 | &interface_mac[1], 265 | &interface_mac[2], 266 | &interface_mac[3], 267 | &interface_mac[4], 268 | &interface_mac[5] 269 | ); 270 | 271 | interface_mtu = xpc_dictionary_get_uint64(params, vmnet_mtu_key); 272 | interface_packet_size = xpc_dictionary_get_uint64(params, vmnet_max_packet_size_key); 273 | 274 | fprintf(stderr, "vmnet mtu: %u\n", (unsigned)interface_mtu); 275 | fprintf(stderr, "vmnet packet size: %u\n", (unsigned)interface_packet_size); 276 | 277 | } 278 | dispatch_semaphore_signal(sem); 279 | }); 280 | if (!interface) { 281 | vmnet_start_interface_failed(); 282 | } 283 | 284 | dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); 285 | 286 | 287 | if (interface_status == VMNET_SUCCESS) { 288 | buffer_size = (interface_packet_size * 2 + 1023) & ~1023; 289 | buffer = (uint8_t *)malloc(buffer_size); 290 | } else { 291 | if (interface) { 292 | vmnet_stop_interface(interface, q, ^(vmnet_return_t status){ 293 | dispatch_semaphore_signal(sem); 294 | }); 295 | dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); 296 | interface = NULL; 297 | } 298 | vmnet_start_interface_failed(); 299 | } 300 | 301 | dispatch_release(sem); 302 | xpc_release(dict); 303 | drop_privileges(); 304 | } 305 | 306 | void vm_shutdown(void) { 307 | 308 | dispatch_queue_t q; 309 | dispatch_semaphore_t sem; 310 | 311 | 312 | if (interface) { 313 | sem = dispatch_semaphore_create(0); 314 | q = dispatch_get_global_queue(QOS_CLASS_UTILITY, 0); 315 | 316 | vmnet_stop_interface(interface, q, ^(vmnet_return_t status){ 317 | dispatch_semaphore_signal(sem); 318 | }); 319 | dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); 320 | dispatch_release(sem); 321 | 322 | interface = NULL; 323 | interface_status = 0; 324 | } 325 | free(buffer); 326 | buffer = NULL; 327 | buffer_size = 0; 328 | 329 | } 330 | 331 | int main(int argc, char **argv) { 332 | 333 | 334 | uint32_t msg; 335 | uint32_t extra; 336 | ssize_t ok; 337 | 338 | 339 | vm_startup(); 340 | 341 | for(;;) { 342 | ok = safe_read(&msg, 4); 343 | if (ok == 0) break; 344 | if (ok != 4) errx(1,"read msg"); 345 | 346 | extra = msg >> 8; 347 | msg = msg & 0xff; 348 | 349 | switch(msg) { 350 | case MSG_STATUS: 351 | msg_status(extra); 352 | break; 353 | case MSG_QUIT: 354 | vm_shutdown(); 355 | exit(0); 356 | case MSG_READ: 357 | msg_read(extra); 358 | break; 359 | case MSG_WRITE: 360 | msg_write(extra); 361 | break; 362 | } 363 | } 364 | 365 | vm_shutdown(); 366 | exit(0); 367 | } 368 | -------------------------------------------------------------------------------- /vmnet_helper/vmnet_helper.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | --------------------------------------------------------------------------------