diff --git a/installer/iris-gui-notarized.entitlements b/installer/iris-gui-notarized.entitlements
new file mode 100644
index 0000000..85b08f9
--- /dev/null
+++ b/installer/iris-gui-notarized.entitlements
@@ -0,0 +1,26 @@
+
+
+
+
+
+ com.apple.security.cs.allow-jit
+
+ com.apple.security.cs.allow-unsigned-executable-memory
+
+
+
diff --git a/iris-gui/Cargo.toml b/iris-gui/Cargo.toml
index 9e1a508..20efd53 100644
--- a/iris-gui/Cargo.toml
+++ b/iris-gui/Cargo.toml
@@ -33,10 +33,17 @@ assets = [
]
[features]
-# Mac App Store distribution build. Hides the CI/Automation config tab (the
-# iris-ci socket is a developer automation feature unusable in the sandbox)
-# and any other developer-only affordances. Set by .github/workflows/appstore.yml.
-appstore = []
+# Pre-compiled / distributed build. Hides developer-only affordances that don't
+# make sense for users running an official binary — currently the iris.toml
+# import/export menu items (the GUI's gui.json machine store is the system of
+# record; iris.toml is only useful alongside the standalone `iris` CLI, i.e. a
+# source checkout). Set by the Release and App Store workflows. A no-op for
+# source builds, where the iris.toml workflow stays available.
+bundled = []
+# Mac App Store distribution build. Implies `bundled`, and additionally hides
+# the CI/Automation config tab (the iris-ci socket is a developer automation
+# feature unusable in the sandbox). Set by .github/workflows/appstore.yml.
+appstore = ["bundled"]
[dependencies]
# Group A (additive) features are always on for iris-gui so the user can
diff --git a/iris-gui/src/config_ui.rs b/iris-gui/src/config_ui.rs
index 431fa89..5f057d2 100644
--- a/iris-gui/src/config_ui.rs
+++ b/iris-gui/src/config_ui.rs
@@ -83,26 +83,59 @@ impl JitEnv {
}
}
-pub fn show_tab(ui: &mut Ui, tab: Tab, cfg: &mut MachineConfig, jit: &mut JitEnv) {
+/// Action a config tab asks the app to perform that needs app-level state
+/// (e.g. a confirmation modal) the immediate-mode tab UI doesn't own.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
+pub enum ConfigAction {
+ #[default]
+ None,
+ /// User clicked "Use embedded PROM"; the app should confirm with the user
+ /// and, if accepted, clear `cfg.prom` (an empty path falls back to the
+ /// built-in PROM in `iris::prom::Prom::from_file_or_embedded`).
+ RequestEmbeddedProm,
+}
+
+pub fn show_tab(ui: &mut Ui, tab: Tab, cfg: &mut MachineConfig, jit: &mut JitEnv) -> ConfigAction {
ScrollArea::vertical().show(ui, |ui| match tab {
Tab::General => show_general(ui, cfg),
- Tab::Disks => show_disks(ui, cfg),
- Tab::Network => show_network(ui, cfg),
- Tab::Memory => show_memory(ui, cfg),
- Tab::Display => show_display(ui, cfg),
- Tab::VideoIn => show_vino(ui, cfg),
- Tab::Debug => show_debug(ui, cfg, jit),
- Tab::Ci => show_ci(ui, cfg),
- });
+ Tab::Disks => { show_disks(ui, cfg); ConfigAction::None }
+ Tab::Network => { show_network(ui, cfg); ConfigAction::None }
+ Tab::Memory => { show_memory(ui, cfg); ConfigAction::None }
+ Tab::Display => { show_display(ui, cfg); ConfigAction::None }
+ Tab::VideoIn => { show_vino(ui, cfg); ConfigAction::None }
+ Tab::Debug => { show_debug(ui, cfg, jit); ConfigAction::None }
+ Tab::Ci => { show_ci(ui, cfg); ConfigAction::None }
+ }).inner
}
-fn show_general(ui: &mut Ui, cfg: &mut MachineConfig) {
+fn show_general(ui: &mut Ui, cfg: &mut MachineConfig) -> ConfigAction {
+ let mut action = ConfigAction::None;
ui.heading("General");
Grid::new("general_grid").num_columns(2).striped(true).show(ui, |ui| {
ui.label("PROM image");
path_row(ui, "prom", &mut cfg.prom, Pick::OpenFile, PROM_FILTERS);
ui.end_row();
+ // Leaving the PROM path empty boots the built-in PROM. Expose that as
+ // an explicit button so reverting from a (possibly missing) custom PROM
+ // is discoverable instead of "delete the text by hand". Disabled when
+ // already empty, so the confirm prompt only ever appears when a custom
+ // PROM is selected.
+ ui.label("");
+ ui.horizontal(|ui| {
+ let custom = !cfg.prom.is_empty();
+ if ui.add_enabled(custom, egui::Button::new("Use embedded PROM"))
+ .on_hover_text("Boot IRIS's built-in PROM instead of a file")
+ .clicked()
+ {
+ action = ConfigAction::RequestEmbeddedProm;
+ }
+ if !custom {
+ ui.label(RichText::new("(using built-in PROM)").weak());
+ }
+ });
+ ui.end_row();
+
ui.label("NVRAM file");
path_row(ui, "nvram", &mut cfg.nvram, Pick::SaveFile, NVRAM_FILTERS);
ui.end_row();
@@ -111,6 +144,7 @@ fn show_general(ui: &mut Ui, cfg: &mut MachineConfig) {
path_row_opt(ui, "serial_log", &mut cfg.serial_log, Pick::SaveFile, ANY_FILTERS);
ui.end_row();
});
+ action
}
fn show_memory(ui: &mut Ui, cfg: &mut MachineConfig) {
diff --git a/iris-gui/src/main.rs b/iris-gui/src/main.rs
index a1a33ed..7b2ef48 100644
--- a/iris-gui/src/main.rs
+++ b/iris-gui/src/main.rs
@@ -11,7 +11,7 @@ mod scsi_menu;
mod settings;
mod single_instance;
-use config_ui::{cfg_to_toml, show_tab, JitEnv, Tab};
+use config_ui::{cfg_to_toml, show_tab, ConfigAction, JitEnv, Tab};
use dialogs::create_disk::CreateDiskDialog;
use dialogs::new_machine::{distribute_ram, NewMachineDialog};
use eframe::egui;
@@ -115,6 +115,8 @@ struct App {
fullscreen: bool,
stop_modal: Option,
missing_modal: Option,
+ /// Set when the user clicks "Use embedded PROM"; drives a confirmation modal.
+ confirm_embedded_prom: bool,
new_machine: NewMachineDialog,
create_disk: CreateDiskDialog,
/// If true, central panel shows the tabbed config editor; otherwise the
@@ -227,6 +229,7 @@ impl App {
toast: None,
stop_modal: None,
missing_modal: None,
+ confirm_embedded_prom: false,
new_machine,
create_disk: CreateDiskDialog::default(),
show_config_editor: false,
@@ -455,26 +458,32 @@ impl App {
}
ui.close_menu();
}
- ui.separator();
- if ui.button("Import iris.toml…").clicked() {
- if let Some(path) = native_open_dialog("Import iris.toml", &[("TOML", &["toml"])]) {
- let cfg = MachineConfig::load_toml(&path.to_string_lossy());
- let stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or("imported");
- let name = self.prefs.unique_name(stem);
- self.prefs.machines.insert(name.clone(), cfg.clone());
- self.prefs.active_machine = Some(name.clone());
- self.cfg = cfg;
- self.cfg_path = Some(path);
- self.flush_machine();
- self.toast(format!("imported as '{name}'"));
+ // iris.toml import/export is a source-build affordance for users
+ // who also run the standalone `iris` CLI; the GUI's own gui.json
+ // machine store is the system of record. Hidden in pre-compiled /
+ // App Store builds (the `bundled` feature). See iris-gui Cargo.toml.
+ if !cfg!(feature = "bundled") {
+ ui.separator();
+ if ui.button("Import iris.toml…").clicked() {
+ if let Some(path) = native_open_dialog("Import iris.toml", &[("TOML", &["toml"])]) {
+ let cfg = MachineConfig::load_toml(&path.to_string_lossy());
+ let stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or("imported");
+ let name = self.prefs.unique_name(stem);
+ self.prefs.machines.insert(name.clone(), cfg.clone());
+ self.prefs.active_machine = Some(name.clone());
+ self.cfg = cfg;
+ self.cfg_path = Some(path);
+ self.flush_machine();
+ self.toast(format!("imported as '{name}'"));
+ }
+ ui.close_menu();
}
- ui.close_menu();
- }
- if ui.button("Export current to iris.toml…").clicked() {
- if let Some(path) = native_save_dialog("Export iris.toml", &[("TOML", &["toml"])]) {
- self.save_config(path);
+ if ui.button("Export current to iris.toml…").clicked() {
+ if let Some(path) = native_save_dialog("Export iris.toml", &[("TOML", &["toml"])]) {
+ self.save_config(path);
+ }
+ ui.close_menu();
}
- ui.close_menu();
}
ui.separator();
if ui.button("Quit").clicked() {
@@ -754,7 +763,10 @@ impl App {
}
});
ui.separator();
- show_tab(ui, self.tab, &mut self.cfg, &mut self.jit);
+ match show_tab(ui, self.tab, &mut self.cfg, &mut self.jit) {
+ ConfigAction::RequestEmbeddedProm => self.confirm_embedded_prom = true,
+ ConfigAction::None => {}
+ }
}
fn welcome_panel(&mut self, ui: &mut egui::Ui) {
@@ -977,6 +989,32 @@ impl eframe::App for App {
}
}
+ // Confirm switching from a custom PROM back to the built-in image.
+ if self.confirm_embedded_prom {
+ let prom = self.cfg.prom.clone();
+ let mut close = false;
+ let mut do_clear = false;
+ egui::Window::new("Use embedded PROM?")
+ .collapsible(false)
+ .resizable(false)
+ .anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0])
+ .show(ctx, |ui| {
+ ui.label(RichText::new("Replace the selected PROM with IRIS's built-in PROM?").strong());
+ ui.label(RichText::new(&prom).weak());
+ ui.add_space(8.0);
+ ui.horizontal(|ui| {
+ if ui.button("Cancel").clicked() { close = true; }
+ if ui.button("Use embedded PROM").clicked() { do_clear = true; close = true; }
+ });
+ });
+ if do_clear {
+ self.cfg.prom.clear();
+ self.mark_dirty();
+ self.toast("using embedded PROM");
+ }
+ if close { self.confirm_embedded_prom = false; }
+ }
+
// Missing-disk modal.
enum MissingChoice { None, Cancel, Detach, EditDisks }
let mut choice = MissingChoice::None;