Skip to content

Complete macOS app for MPF: signed universal .app with auto-signed dumping tools#977

Open
knutwurst wants to merge 48 commits into
SabreTools:masterfrom
knutwurst:macos-ui
Open

Complete macOS app for MPF: signed universal .app with auto-signed dumping tools#977
knutwurst wants to merge 48 commits into
SabreTools:masterfrom
knutwurst:macos-ui

Conversation

@knutwurst
Copy link
Copy Markdown

@knutwurst knutwurst commented May 27, 2026

Complete macOS app for MPF

MPF's GUI is WPF/WinForms and therefore Windows-only. This PR delivers a complete, ready-to-run macOS application: a signed, universal .app bundle containing the full MPF UI, with native macOS window controls, light/dark themes, and the bundled dumping tools made runnable out of the box. The UI is built with Avalonia and reuses the existing MPF.Frontend view-models unchanged — it's a second View layer over the same MVVM core, not a logic rewrite.

Relationship to #975: #975 is the primary cross-platform Avalonia-UI effort and is further along. This PR's core UI port overlaps with it, but its real focus is the complete macOS app — the signed .app, dock icon, native chrome, and on-launch signing of the bundled tools — i.e. the "icons and niceties on macOS" that #975 mentions deferring to a later PR. I'm glad to rebase just those macOS-specific pieces on top of #975 and contribute them there instead of landing a parallel UI (see the discussion below).

Screenshots

Light Dark
Main window Main – light Main – dark
Options Options – light Options – dark

The macOS app

  • Signed universal (arm64 + x64) .app — one bundle runs natively on Apple Silicon and Intel; built by MPF.UI.Avalonia/build-macos-app.sh.
  • App icon wired for the window and the macOS dock/Finder.
  • Native window decorations (real macOS traffic-light controls, native drag/zoom/close), the OS-style system accent, and the native system font (San Francisco) so it doesn't feel like a Windows port.
  • Zero-friction dumping tools: on launch the app strips the quarantine flag from, ad-hoc code-signs, and adds the needed @executable_path/lib rpath to the bundled tools under a sibling Programs/ folder — so redumper/DiscImageCreator run on Apple Silicon with no manual Gatekeeper/codesign steps.
  • Live tool output in the log: the dumping program's stdout/stderr (incl. redumper's carriage-return progress) is streamed into the in-app log. Opt-in per frontend, so Windows/WPF keeps its console behavior.

Full UI parity

  • Every window is ported — Main, Options, Media Information, Check Dump, Create IRD and the Ring Code Guide — plus the LogOutput/UserInput controls and a message-box replacement.
  • Binds directly to the unchanged MPF.Frontend view-models (MainViewModel, OptionsViewModel, MediaInformationViewModel, CheckDumpViewModel, CreateIRDViewModel).
  • Light & dark themes; all 12 interface languages ported and switchable at runtime.

Build & CI

  • publish-nix.sh builds and zips MPF.UI.Avalonia for every desktop runtime using the same naming scheme as the existing projects (MPF.UI.Avalonia_<framework>_<runtime>_<config>.zip), bundling the dumping tools under Programs/.
  • The Build & Test workflow gains a workflow_dispatch trigger and a separate macOS-runner job that publishes the signed universal .app and attaches it to the rolling release. (That job uses macOS CI minutes — happy to gate it behind workflow_dispatch/tags if you prefer.)

macOS-specific fixes to shared code

Each also benefits the CLI on macOS:

  • DefaultDiscImageCreatorPath now detects macOS via RuntimeInformation (OSVersion.Platform returns Unix on macOS, so it previously picked the Linux .out binary name).
  • Default output names use the volume name instead of the full mount path (no more _Volumes_DVD_ROM).
  • On macOS, config.json is stored under ~/.config/mpf rather than inside the app bundle.

WPF-only constructs were replaced inside the new project only (CustomMessageBox → a small Avalonia dialog, WinForms file dialogs → StorageProvider, System.Windows.MediaAvalonia.Media, FindResource → Avalonia resource lookups, etc.).

Notes for review

  • The screenshots above are committed under docs/screenshots/ purely for this description and can be moved to attachments before merge.
  • The macOS-runner CI job is optional and easy to drop or gate.
  • The rpath auto-fix uses install_name_tool (Xcode Command Line Tools); if those are absent, signing still works and only the rpath fix is skipped.

macOS app contributed by Knutwursthttps://github.com/knutwurst/MPF

knutwurst added 30 commits May 27, 2026 20:06
Copy all 191 English strings from MPF.UI/Resources/Strings.xaml into
MPF.UI.Avalonia/Resources/Strings.axaml (Avalonia ResourceDictionary,
assembly=System.Runtime) and merge it into App.axaml via ResourceInclude.
…services

Ports Theme, Constants (DoubleCollection -> List<double>), ElementConverter,
MessageBoxWindow, DialogService (sync/async bridge), and StorageDialogs from
MPF.UI to MPF.UI.Avalonia; build passes with 0 warnings.
Add Brushes.axaml with 30 SolidColorBrush resource-key defaults (sourced
from MPF.UI/App.xaml where present, sensible light-theme defaults for the
6 WPF SystemColors keys), and Controls.axaml with HeaderedContentControl
.groupbox and Button.custom styles.  Both are wired into App.axaml.
Adds MPF.UI.Avalonia/Controls/LogOutput.axaml + .axaml.cs, translating
the WPF RichTextBox/FlowDocument log panel to an Avalonia
SelectableTextBlock with inline Run coloring inside a ScrollViewer.
Preserves the public EnqueueLog(LogLevel, string) API for wiring to
MainViewModel.Init in Task 8.
Adds UserInput.axaml/.cs with full StyledProperty parity to the WPF
DependencyProperty API, plus StringToDoubleConverter to bridge the
string TextHeight property to Avalonia's double Height.
Port the WPF MainWindow + WindowBase to Avalonia: settings/controls/
status group boxes, custom title-bar chrome, menu, and log output, wired
to MainViewModel.Init with the DialogService message-box bridge and a
ShowMediaInformationWindow stub. Secondary windows (Options/CheckDump/
CreateIRD) are stubbed pending later tasks. Also fix LogOutput so its
named controls resolve when loaded via AvaloniaXamlLoader.
- Add OptionsWindow.axaml with all five tabs (General, Paths, Dumping,
  Programs, Login Info) including every GroupBox/CheckBox/ComboBox/Slider
  from the WPF original; uses x:CompileBindings="False" for deep VM paths.
- Add OptionsWindow.axaml.cs: parameterless ctor for XAML compiler + real
  ctor(Options); Browse buttons via StorageDialogs.OpenFileAsync /
  PickFolderAsync; Accept/Cancel set SavedSettings; Redump login test calls
  RedumpClient.ValidateCredentials and shows result via MessageBoxWindow;
  Slider.Ticks populated in code-behind as AvaloniaList<double> (Avalonia
  does not accept List<double> in XAML binding for that property).
- Replace ShowOptionsWindow stub in MainWindow.axaml.cs with modal
  ShowDialog + inline OnOptionsUpdated: refreshes output path, applies
  language change, calls MainViewModel.UpdateOptions, re-applies theme
  and media-type visibility.
…ndings (Avalonia lacks WPF enum-cast indexer syntax)
- Add CheckDumpWindow.axaml: custom-chrome window with Settings group
  (input path + Browse, system combo, program combo), Status group,
  warning label, and Check Dump / Cancel buttons; inherits WindowBase.
- Add CheckDumpWindow.axaml.cs: ctor sets DataContext to CheckDumpViewModel;
  BrowseFile uses StorageDialogs.OpenFileAsync; SelectionChanged handlers for
  system/program/input are guarded by CanExecuteSelectionChanged; CheckDump
  runs on a background thread via Task.Run to avoid UI-thread deadlock;
  ShowMediaInformationWindow bridges the synchronous ProcessUserInfoDelegate
  to MediaInformationWindow via Dispatcher.UIThread.InvokeAsync.
- Replace ShowCheckDumpWindow stub in MainWindow with hide/show UX matching WPF.
Translates MPF.UI/Windows/CreateIRDWindow.xaml(.cs) to Avalonia:
- CreateIRDWindow.axaml: custom chrome title bar, Input group (ISO path +
  .getkey.log path with Browse buttons), Key/DiscID/PIC Expanders, Status
  group, Create IRD / Cancel buttons; all *Enabled bindings wired.
- CreateIRDWindow.axaml.cs: ctor sets DataContext = new CreateIRDViewModel();
  five async Browse helpers (ISO, Log, Key, PIC open-pickers; IRD save-picker)
  using StorageDialogs; TextChanged/Browse Click handlers all guarded by
  CanExecuteSelectionChanged; OnCreateIRDClick collapses expanders, disables UI,
  prompts for output path via BrowseOutputFile, runs synchronous
  CreateIRDViewModel.CreateIRD(outputPath) on a background thread via Task.Run,
  then shows success (YesNo → reset or close) or failure (Ok) MessageBoxWindow.
- MainWindow.axaml.cs: replaces the "coming soon" async stub with the real
  ShowCreateIRDWindow() (Hide → new CreateIRDWindow(this) → Closed → Show +
  Activate), matching WPF UX.
…de button

Ports the WPF RingCodeGuideWindow to Avalonia: custom title-bar with
minimize/close Path-buttons (using WindowBase handlers), two-tab layout
showing annotated ring-code guide images (1-layer and 2-layer), and all
explanatory Run/TextBlock text. Images bundled as AvaloniaResource
(avares://MPF.Avalonia/Images/). MediaInformationWindow.OnRingCodeGuideClick
stub replaced with a non-async handler that opens the real window via
win.Show(this).
…ming

Add DarkModeTheme, LightModeTheme, and CustomTheme under MPF.UI.Avalonia/Themes/
and replace the ApplyTheme() no-op in MainWindow with the same option-based
selection logic as the WPF source; also sets RequestedThemeVariant so Fluent
controls follow dark/light mode.
…k-mode OSes

App used RequestedThemeVariant=Default which follows the OS. On a dark-mode
host, FluentTheme rendered control text white while the custom theme brushes
hardcode a light (white) window background, making checkboxes, tab headers and
buttons invisible. MPF's WPF UI is light-by-default with an opt-in dark mode, so
pin the variant to Light; ApplyTheme() still switches to Dark when the user
enables dark mode (verified: dark path uses dark background + light text).
…ndows-style title bar

Remove SystemDecorations="None", TransparencyLevelHint, and Background="Transparent"
from all 6 windows; unwrap the outer Border; delete the hand-drawn title-bar Grid
(icon Image, centered TextBlock with ContextMenu, Path-based min/close Buttons).
Each window now sets Background="{DynamicResource WindowBrush}" and relies on the
native macOS traffic-light chrome for close/minimize/zoom.

MainWindow keeps its menu bar (File/Tools/Help/Languages + new Window menu with
Minimize and Close items) rendered as a plain <Menu> at the top of the StackPanel.
OptionsWindow title defaults to OptionsTitleString in XAML; the caller may override
it (first-run title) via Window.Title as before. CreateIRDWindow title switched to
the dedicated CreatePS3IRDTitleString resource.

WindowBase.cs is unchanged (TitleMouseDown left in place; CloseButtonClick /
MinimizeButtonClick still used by the Window menu items in MainWindow).
…volume picker, program discovery

- Custom buttons now use theme brushes + explicit foreground so labels are legible in dark mode (were white-on-light-grey).
- Shrink oversized FluentTheme tab-header font.
- Drive picker shows the device/volume path (Name) instead of a Windows drive letter; relabel 'Drive Letter' -> 'Drive' / 'Laufwerk'.
- Set working directory to the folder next to the .app bundle at startup so relative 'Programs/<tool>' paths resolve (fixes empty dumping-program list / 'No dumping program found').
…able/untruncated buttons, scrollable resizable main window

- Options.DefaultDiscImageCreatorPath: detect macOS via RuntimeInformation (Environment.OSVersion.Platform returns Unix on macOS, so the default wrongly used the Linux 'DiscImageCreator.out' name instead of 'DiscImageCreator'). Conditional-compiled to keep old TFMs building. Also fixes the CLI on macOS.
- Reduce app-wide font (ControlContentThemeFontSize + Window FontSize) so button labels stop truncating; shrink oversized tab headers.
- MainWindow: wrap content in a ScrollViewer and make the window resizable (was SizeToContent + non-resizable, which pushed the log's Clear/Save buttons off-screen).
- LogOutput Clear/Save buttons: use the real 'custom' style class (was 'CustomButton', a no-op).
…uttons visible

Dock the top sections, let the log Expander fill the remaining height, and pin the
log's Clear/Save buttons to the bottom of the control (DockPanel) so they no longer
get clipped off the bottom of the window. Log area background made dark to fill cleanly.
On macOS, strip the download quarantine flag and apply an ad-hoc code signature to
every Mach-O file under the sibling Programs/ folder at launch. This lets bundled
helpers (redumper, DiscImageCreator, their dylibs) run without the user manually
clearing Gatekeeper — and is required on Apple Silicon, which refuses to run
unsigned binaries. Best-effort: silently no-ops if xattr/codesign are unavailable.
…so dyld finds their libs

redumper ships its dylibs in a sibling lib/ folder but its only rpath is
@executable_path/../lib, so dyld looked one level too high and the tool was killed
('cannot be opened'). On startup, for each bundled executable that has a sibling lib/
folder, add an @executable_path/lib rpath (then re-sign). Verified: redumper runs.

Note: DiscImageCreator is unaffected here — it is x86_64 and hard-links
/opt/local/lib/libarchive.13.dylib (MacPorts), which must be installed separately.
Publish both osx-arm64 and osx-x64, fuse the native apphost/dylibs with lipo, and
ad-hoc sign per-file so one bundle runs natively on Apple Silicon and Intel Macs.
Single-arch builds remain available via an explicit RID argument. README updated.
knutwurst added 14 commits May 27, 2026 20:55
…imes

publish-nix.sh now builds and zips the cross-platform Avalonia UI with the same scheme
as the CLI (MPF.UI.Avalonia_<framework>_<runtime>_<config>.zip), bundling the dumping
tools under Programs/, for net10.0 across win-x64/win-arm64/linux-x64/linux-arm64/osx-x64/osx-arm64.
The Build-and-Test workflow gains workflow_dispatch (manual runs on any branch) and links
the new macOS/Linux/Windows Avalonia UI artifacts in the rolling release notes.
…rsal .app

Adds a build-macos-app job (macos-latest, needs: build) that runs build-macos-app.sh
to produce the lipo'd universal (arm64+x64) ad-hoc-signed .app, zips it, and appends
MPF.UI.Avalonia_macos_universal_release.zip to the rolling release (without replacing the
other artifacts). build-macos-app.sh now falls back to the PATH dotnet when no repo-local
./.dotnet exists, so it works on CI runners as well as developer machines.
The internal tools (redumper, DiscImageCreator, ...) were launched with UseShellExecute
and no redirection, so on Windows they got their own console window and on macOS their
output was invisible. Add an optional OutputReceived handler to BaseExecutionContext: when
set, stdout/stderr are captured (UseShellExecute=false, CreateNoWindow) and forwarded
line-by-line; when null, the legacy console-window behavior is preserved. DumpEnvironment
forwards the handler, and MainViewModel routes it to the logger so the program's live
output appears in the log panel.

Note: when a handler is attached, the tool no longer opens a separate console window.
Read the internal program's stdout/stderr raw on dedicated threads and distinguish
carriage-return progress updates (\r) from committed lines (\n) via a ProgramOutputHandler
delegate (net20-safe, replaces Action<string>). The Avalonia LogOutput overwrites the
current line in place for \r updates so progress (e.g. redumper's percentage) animates on a
single line instead of spamming new lines. Capture is opt-in per frontend (MainViewModel.
ProgramOutputSink), so Windows/WPF keeps its console window when no sink is attached.
The dumping program's output is now streamed into the in-app log, so the hint to look at
a separate console window is no longer accurate.
Use the OS system font (San Francisco on macOS) instead of bundling Inter, switch the Fluent
accent to the macOS system blue (#007AFF light / #0A84FF dark), and soften the group boxes and
custom buttons (subtle translucent borders, larger corner radius) so the UI no longer looks
like a Windows port.
…ut names

On Unix/macOS DriveInfo.VolumeLabel is the mount path (e.g. /Volumes/DVD_ROM), which produced
output folders like '_Volumes_DVD_ROM'. Use the final path component (e.g. 'DVD_ROM') instead.
….app bundle

Settings already load/save via OptionsLoader, but on macOS the 'portable' config was created
inside the rebuild-volatile .app bundle and lost on every rebuild. Prefer ~/.config/mpf so
settings persist like they do on Windows.
Set the window icon from the existing Icon.ico and bundle an AppIcon.icns (generated from it)
in the macOS .app via CFBundleIconFile so the dock/Finder show the MPF icon.
…rst)

Add a short author header to the new Avalonia source files and update the csproj authors,
About box, and README to credit Knutwurst (https://github.com/knutwurst).
@mnadareski
Copy link
Copy Markdown
Member

There is another PR already opened for this functionality: #975

knutwurst added 2 commits May 27, 2026 21:59
… review)

- Skip the @executable_path/lib rpath when already present (no duplicate-rpath error on
  relaunch) and only re-sign binaries that changed or aren't validly signed yet.
- Drain stdout before WaitForExit in the process helper to avoid a pipe-buffer deadlock; add
  otool/codesign --verify checks via that helper.
- Isolate per-file failures in PrepareExternalTools so one bad file doesn't abort the rest.
- Skip Java .class files in IsMachO (shared 0xCAFEBABE magic).
- LogOutput.SaveInlines: use File.Create (truncate) so re-saving can't leave stale tail bytes.
- Set MainViewModel.ProgramOutputSink before Init so any env built during init has the sink.
@knutwurst knutwurst changed the title Add a cross-platform (macOS / Linux / Windows) Avalonia UI Complete macOS app for MPF: signed universal .app with auto-signed dumping tools May 27, 2026
@knutwurst
Copy link
Copy Markdown
Author

Thanks — and apologies for the overlap, I hadn't seen #975 when I started. You're right that #975 is the primary Avalonia-UI effort and it's further along.

Where this PR adds something is the complete macOS app experience — essentially the "icons and niceties on macOS" that #975 mentions deferring to a later PR:

  • a signed universal (arm64 + x64) .app with a dock icon and native window controls;
  • on-launch quarantine removal + ad-hoc signing + @executable_path/lib rpath fix of the bundled dumping tools, so redumper/DiscImageCreator run on Apple Silicon with no manual Gatekeeper/codesign steps;
  • the dumping tool's live output streamed into the in-app log; and
  • a few macOS-specific fixes (DiscImageCreator default path, config location, output naming from the volume label).

I'd be glad to rebase just these macOS-specific pieces on top of #975 and contribute them there (or coordinate with @whatev-indus / @Deterous) rather than land a parallel UI — whatever means the least duplicate work for you. Happy to align with #975.

@knutwurst knutwurst marked this pull request as ready for review May 27, 2026 20:21
@Deterous
Copy link
Copy Markdown
Member

Where this PR adds something is the complete macOS app experience — essentially the "icons and niceties on macOS" that #975 mentions deferring to a later PR:

The reason for deferring to a later PR is to minimize the review burden.

Comment thread MPF.Frontend/Drive.cs
TotalSize = driveInfo.TotalSize;
VolumeLabel = driveInfo.VolumeLabel;

// On Unix/macOS DriveInfo.VolumeLabel is the mount path (e.g. "/Volumes/DVD_ROM"),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually redumper needs the BSD drive path for dumping, not the mount path

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Fixed in 2e4d668.

Added a new Drive.DevicePath property that resolves the BSD device node via diskutil info on macOS, with a fallback to the mount path if diskutil isn't available. Drive.Name stays the mount path so the existing filesystem detection (Directory.Exists, Path.Combine on disc contents) keeps working unchanged. The execution contexts for Aaru, DiscImageCreator and Redumper now get DevicePath instead of Name, so redumper sees /dev/disk2 rather than /Volumes/DVD_ROM.


# Builds the signed, universal (arm64 + x64) macOS .app on a macOS runner (needs lipo +
# codesign, which only exist on macOS) and appends it to the same rolling release.
build-macos-app:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Building is preferred in the build script rather than GHA

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, moved into the script in 3fee7c3. build-macos-app.sh now also produces MPF.UI.Avalonia_macos_<mode>_release.zip next to the .app. Naming follows what publish-nix.sh uses for the other targets. The macOS job just runs the script and uploads the file.

knutwurst added 2 commits May 28, 2026 07:25
…path

redumper and DiscImageCreator read raw blocks from the device, so they
need the BSD path (e.g. /dev/disk2). DriveInfo.Name on macOS is the
mount path (/Volumes/DVD_ROM), which the tools can't dump from.

Add a Drive.DevicePath property that resolves the BSD device node via
`diskutil info` on macOS; on Windows / Linux it stays equal to Name.
Hand DevicePath (not Name) to the execution contexts. Name is left as
the mount path so filesystem-level detection (Directory.Exists /
Path.Combine on disc contents) keeps working unchanged.

Falls back to the mount path if diskutil is unavailable.
Keep the packaging logic in one place. The script now emits
`MPF.UI.Avalonia_macos_<mode>_release.zip` next to the .app it produces,
matching the naming used by publish-nix.sh. The macOS-runner job just
runs the script and uploads the artifact.
@knutwurst knutwurst requested a review from Deterous May 28, 2026 12:33
@mnadareski
Copy link
Copy Markdown
Member

I am going to be placing my review effort on the existing PR with regards to all of the Avalonia UI. I definitely will be interested in the Mac build script and that additional setup as well as any improvements that can be made.

I request that you do not make too many changes in your existing PR as I will likely request a stripped-down PR with just the unique functionality from here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants