Repository: pop-os/popsicle (language: rust)

Issue:
Dynamically update device selection view
Watch for devices that are added or removed, and update the list dynamically.


Discussion / hints:

Fix patch:
diff --git a/README.md b/README.md
index af8c804..58ab17e 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,8 @@
 
 Popsicle is a Linux utility for flashing multiple USB devices in parallel, written in [Rust](https://www.rust-lang.org/en-US/).
 
+![GIF Demo](./demo.gif)
+
 ## Build Dependencies
 
 If building the GTK front end, you will be required to install the development dependencies for GTK, usually named `libgtk-3-dev`. No other dependencies are required to build the CLI or GTK front ends, besides Rust's `cargo` utility.
@@ -14,4 +16,4 @@ For those who need to vendor Cargo's crate dependencies which are fetched from [
 
 - `make cli && sudo make install-cli` will build and install just the CLI workspace
 - `make gtk && sudo make install-gtk` will build and install just the GTK workspace
-- `make && sudo make install` will build and install both the CLI and GTK workspaces
\ No newline at end of file
+- `make && sudo make install` will build and install both the CLI and GTK workspaces
diff --git a/demo.gif b/demo.gif
new file mode 100644
index 0000000..05119e7
Binary files /dev/null and b/demo.gif differ
diff --git a/gtk/src/app/content/flashing.rs b/gtk/src/app/content/flashing.rs
index 19746fe..d551d0e 100644
--- a/gtk/src/app/content/flashing.rs
+++ b/gtk/src/app/content/flashing.rs
@@ -9,6 +9,8 @@ pub struct FlashView {
 impl FlashView {
     pub fn new() -> FlashView {
         let progress_list = Grid::new();
+        progress_list.set_row_spacing(6);
+        progress_list.set_column_spacing(6);
         let progress_scroller = ScrolledWindow::new(None, None);
         progress_scroller.add(&progress_list);
 
diff --git a/gtk/src/app/state/device_selection.rs b/gtk/src/app/state/device_selection.rs
new file mode 100644
index 0000000..bcb6814
--- /dev/null
+++ b/gtk/src/app/state/device_selection.rs
@@ -0,0 +1,160 @@
+
+use super::*;
+use gtk;
+use gtk::prelude::*;
+use popsicle;
+
+macro_rules! try_or_error {
+    (
+        $act:expr,
+        $view:expr,
+        $back:expr,
+        $next:expr,
+        $stack:ident,
+        $error:ident,
+        $msg:expr,
+        $val:expr
+    ) => {
+        match $act {
+            Ok(value) => value,
+            Err(why) => {
+                $back.set_visible(false);
+                $next.set_visible(true);
+                $next.set_label("Close");
+                $next.get_style_context().map(|c| {
+                    c.remove_class("destructive-action");
+                    c.remove_class("suggested-action");
+                });
+                $error.set_text(&format!("{}: {:?}", $msg, why));
+                $view.set(2);
+                $stack.set_visible_child_name("error");
+                return $val;
+            }
+        }
+    };
+}
+
+/// Move to device selection screen
+pub fn initialize(
+    state: &State,
+    back: &gtk::Button,
+    error: &gtk::Label,
+    list: &gtk::ListBox,
+    next: &gtk::Button,
+    stack: &gtk::Stack,
+) {
+    back.set_label("Back");
+    back.get_style_context().map(|c| {
+        c.add_class("back-button");
+    });
+    next.set_label("Flash");
+    next.get_style_context().map(|c| {
+        c.remove_class("suggested-action");
+        c.add_class("destructive-action");
+    });
+    stack.set_visible_child_name("devices");
+
+    // Remove all but the first row
+    list.get_children()
+        .into_iter()
+        .for_each(|widget| widget.destroy());
+
+    let mut devices = vec![];
+    if let Err(why) = popsicle::get_disk_args(&mut devices) {
+        eprintln!("popsicle: unable to get devices: {}", why);
+    }
+
+    refresh_device_list(state, &devices, back, error, list, next, stack);
+}
+
+pub fn refresh_device_list(
+    state: &State,
+    devices: &[String],
+    back: &gtk::Button,
+    error: &gtk::Label,
+    list: &gtk::ListBox,
+    next: &gtk::Button,
+    stack: &gtk::Stack,
+) {
+    let device_list = &state.devices;
+    let mut device_list = try_or_error!(
+        device_list.lock(),
+        state.view,
+        back,
+        next,
+        stack,
+        error,
+        "device list mutex lock failure",
+        ()
+    );
+    device_list.clear();
+
+    list.get_children().iter().for_each(|c| c.destroy());
+
+    for device in devices {
+        // Attempt to get the canonical path of the device.
+        // Display the error view if this fails.
+        let name = try_or_error!(
+            Path::new(&device).canonicalize(),
+            state.view,
+            back,
+            next,
+            stack,
+            error,
+            format!("unable to get canonical path of '{}'", device),
+            ()
+        );
+
+        let button = if let Some(block) = BlockDevice::new(&name) {
+            CheckButton::new_with_label(&[
+                &block.label(),
+                " (",
+                &name.to_string_lossy(),
+                ")",
+            ].concat())
+        } else {
+            CheckButton::new_with_label(&name.to_string_lossy())
+        };
+
+        list.insert(&button, -1);
+        device_list.push((device.clone(), button));
+    }
+
+    list.show_all();
+}
+
+pub fn device_requires_refresh(
+    state: &State,
+    back: &gtk::Button,
+    error: &gtk::Label,
+    next: &gtk::Button,
+    stack: &gtk::Stack,
+) -> Option<Vec<String>> {
+    let device_list = try_or_error!(
+        state.devices.lock(),
+        state.view,
+        back,
+        next,
+        stack,
+        error,
+        "device list mutex lock failure",
+        None
+    );
+
+    let mut devices = vec![];
+    if let Err(why) = popsicle::get_disk_args(&mut devices) {
+        eprintln!("popsicle: unable to get devices: {}", why);
+    }
+
+    if devices.len() != device_list.len() || devices_differ(&devices, &device_list) {
+        Some(devices)
+    } else {
+        None
+    }
+}
+
+fn devices_differ(devices: &[String], device_list: &[(String, gtk::CheckButton)]) -> bool {
+    devices.iter()
+        .zip(device_list.iter())
+        .any(|(ref x, &(ref y, _))| x.as_str() != y.as_str())
+}
diff --git a/gtk/src/app/state/flash_devices.rs b/gtk/src/app/state/flash_devices.rs
new file mode 100644
index 0000000..610084b
--- /dev/null
+++ b/gtk/src/app/state/flash_devices.rs
@@ -0,0 +1,224 @@
+use super::*;
+use gtk;
+use gtk::prelude::*;
+use std::sync::{Arc, Mutex};
+use popsicle::{self, DiskError};
+use std::sync::atomic::Ordering;
+use std::thread;
+use std::mem;
+use std::time::Instant;
+use std::path::Path;
+
+macro_rules! try_or_error {
+    (
+        $act:expr,
+        $view:expr,
+        $back:expr,
+        $next:expr,
+        $stack:ident,
+        $error:ident,
+        $msg:expr,
+        $val:expr
+    ) => {
+        match $act {
+            Ok(value) => value,
+            Err(why) => {
+                $back.set_visible(false);
+                $next.set_visible(true);
+                $next.set_label("Close");
+                $next.get_style_context().map(|c| {
+                    c.remove_class("destructive-action");
+                    c.remove_class("suggested-action");
+                });
+                $error.set_text(&format!("{}: {:?}", $msg, why));
+                $view.set(2);
+                $stack.set_visible_child_name("error");
+                return $val;
+            }
+        }
+    };
+}
+
+/// Begins the flashing process.
+pub fn flash_devices(
+    state: &State,
+    back: &gtk::Button,
+    error: &gtk::Label,
+    next: &gtk::Button,
+    stack: &gtk::Stack,
+    summary_grid: &gtk::Grid,
+) {
+    let task_handles = &state.task_handles;
+    let bars = &state.bars;
+    let start = &state.start;
+    let tasks = &state.tasks;
+
+    let (_, ref mut image_data) = *try_or_error!(
+        state.buffer.data.lock(),
+        state.view,
+        back,
+        next,
+        stack,
+        error,
+        "mutex lock failure",
+        ()
+    );
+
+    let device_list = try_or_error!(
+        state.devices.lock(),
+        state.view,
+        back,
+        next,
+        stack,
+        error,
+        "device list mutex lock failure",
+        ()
+    );
+    let devs = device_list
+        .iter()
+        .filter(|x| x.1.get_active())
+        .map(|x| x.0.clone());
+
+    let mounts = try_or_error!(
+        popsicle::Mount::all(),
+        state.view,
+        back,
+        next,
+        stack,
+        error,
+        "unable to obtain mount points",
+        ()
+    );
+
+    let disks = try_or_error!(
+        popsicle::disks_from_args(devs, &mounts, true),
+        state.view,
+        back,
+        next,
+        stack,
+        error,
+        "unable to get collect devices",
+        ()
+    );
+
+    back.get_style_context().map(|c| {
+        c.remove_class("back-button");
+        c.add_class("destructive-action");
+    });
+
+    back.set_label("Cancel");
+    back.set_visible(true);
+    next.set_visible(false);
+    stack.set_visible_child_name("flash");
+
+    // Clear the progress bar summaries.
+    let mut bars = bars.borrow_mut();
+    bars.clear();
+    summary_grid.get_children().iter().for_each(|c| c.destroy());
+
+    *start.borrow_mut() = Instant::now();
+    let mut tasks = try_or_error!(
+        tasks.lock(),
+        state.view,
+        back,
+        next,
+        stack,
+        error,
+        "tasks mutex lock failure",
+        ()
+    );
+
+    let mut task_handles = try_or_error!(
+        task_handles.lock(),
+        state.view,
+        back,
+        next,
+        stack,
+        error,
+        "task handles mutex lock failure",
+        ()
+    );
+
+    // Take ownership of the data, so that we may wrap it within an `Arc`
+    // and redistribute it across threads.
+    //
+    // Note: Possible optimization could be done to avoid the wrap.
+    //       Avoiding the wrap could eliminate two allocations.
+    let mut data = Vec::new();
+    mem::swap(&mut data, image_data);
+    let image_data = Arc::new(data);
+
+    for (id, (disk_path, mut disk)) in disks.into_iter().enumerate() {
+        let id = id as i32;
+        let image_data = image_data.clone();
+        let progress = Arc::new(AtomicUsize::new(0));
+        let finished = Arc::new(AtomicUsize::new(0));
+        let bar = ProgressBar::new();
+        bar.set_hexpand(true);
+
+        let label = {
+            let disk_path = try_or_error!(
+                Path::new(&disk_path).canonicalize(),
+                state.view,
+                back,
+                next,
+                stack,
+                error,
+                format!("unable to get canonical path of {}", disk_path),
+                ()
+            );
+            if let Some(block) = BlockDevice::new(&disk_path) {
+                gtk::Label::new(
+                    [&block.label(), " (", &disk_path.to_string_lossy(), ")"]
+                        .concat()
+                        .as_str(),
+                )
+            } else {
+                gtk::Label::new(disk_path.to_string_lossy().as_ref())
+            }
+        };
+
+        label.set_justify(gtk::Justification::Right);
+        label.get_style_context().map(|c| c.add_class("bold"));
+        let bar_label = gtk::Label::new("");
+        bar_label.set_halign(gtk::Align::Center);
+        let bar_container = gtk::Box::new(Orientation::Vertical, 0);
+        bar_container.pack_start(&bar, false, false, 0);
+        bar_container.pack_start(&bar_label, false, false, 0);
+        summary_grid.attach(&label, 0, id, 1, 1);
+        summary_grid.attach(&bar_container, 1, id, 1, 1);
+        bars.push((bar, bar_label));
+
+        // Spawn a thread that will update the progress value over time.
+        //
+        // The value will be stored within an intermediary atomic integer,
+        // because it is unsafe to send GTK widgets across threads.
+        task_handles.push({
+            let progress = progress.clone();
+            let finished = finished.clone();
+            thread::spawn(move || -> Result<(), DiskError> {
+                let result = popsicle::write_to_disk(
+                    |_msg| (),
+                    || (),
+                    |value| progress.store(value as usize, Ordering::SeqCst),
+                    disk,
+                    disk_path,
+                    image_data.len() as u64,
+                    &image_data,
+                    false,
+                );
+
+                finished.store(1, Ordering::SeqCst);
+                result
+            })
+        });
+
+        tasks.push(FlashTask {
+            previous: Arc::new(Mutex::new([0; 7])),
+            progress,
+            finished,
+        });
+    }
+
+    summary_grid.show_all();
+}
diff --git a/gtk/src/app/state.rs b/gtk/src/app/state/mod.rs
similarity index 59%
rename from gtk/src/app/state.rs
rename to gtk/src/app/state/mod.rs
index 14f59ca..7278e95 100644
--- a/gtk/src/app/state.rs
+++ b/gtk/src/app/state/mod.rs
@@ -1,3 +1,10 @@
+mod device_selection;
+mod flash_devices;
+#[macro_use]
+mod try;
+
+use self::flash_devices::flash_devices;
+
 use super::super::BlockDevice;
 use super::{App, OpenDialog};
 
@@ -16,7 +23,7 @@ use std::time::{Duration, Instant};
 
 use gtk;
 use gtk::*;
-use popsicle::{self, DiskError};
+use popsicle::DiskError;
 
 /// Contains all of the state that needs to be shared across the program's lifetime.
 pub struct State {
@@ -67,35 +74,8 @@ pub struct FlashTask {
     finished: Arc<AtomicUsize>,
 }
 
-macro_rules! try_or_error {
-    (
-        $act:expr,
-        $view:expr,
-        $back:expr,
-        $next:expr,
-        $stack:ident,
-        $error:ident,
-        $msg:expr,
-        $val:expr
-    ) => {
-        match $act {
-            Ok(value) => value,
-            Err(why) => {
-                $back.set_visible(false);
-                $next.set_visible(true);
-                $next.set_label("Close");
-                $next.get_style_context().map(|c| {
-                    c.remove_class("destructive-action");
-                    c.remove_class("suggested-action");
-                });
-                $error.set_text(&format!("{}: {:?}", $msg, why));
-                $view.set(2);
-                $stack.set_visible_child_name("error");
-                return $val;
-            }
-        }
-    };
-}
+
+
 
 pub struct Connected(App);
 
@@ -200,7 +180,10 @@ impl Connect for App {
         back.connect_clicked(move |back| {
             let view = state.view.get();
             match view {
-                0 => gtk::main_quit(),
+                0 => {
+                    gtk::main_quit();
+                    return;
+                },
                 1 => {
                     stack.set_transition_type(StackTransitionType::SlideRight);
                     stack.set_visible_child_name("image");
@@ -215,6 +198,10 @@ impl Connect for App {
                         c.add_class("suggested-action");
                     });
                 }
+                2 => {
+                    gtk::main_quit();
+                    return;
+                }
                 _ => unreachable!(),
             }
 
@@ -232,257 +219,63 @@ impl Connect for App {
         let summary_grid = self.content.flash_view.progress_list.clone();
         let state = self.state.clone();
         let error = self.content.error_view.view.description.clone();
+        let all = self.content.devices_view.select_all.clone();
+
+        fn watch_device_selection(
+            state: Arc<State>,
+            all: gtk::CheckButton,
+            back: gtk::Button,
+            error: gtk::Label,
+            list: gtk::ListBox,
+            next: gtk::Button,
+            stack: gtk::Stack,
+        ) {
+            gtk::timeout_add(16, move || {
+                if state.view.get() == 1 {
+                    match device_selection::device_requires_refresh(&state, &back, &error, &next, &stack) {
+                        Some(devices) => {
+                            device_selection::refresh_device_list(&state, &devices, &back, &error, &list, &next, &stack);
+                            all.set_active(false);
+                            next.set_sensitive(false);
+                        }
+                        None => if let Ok(ref mut devices) = state.devices.try_lock() {
+                            next.set_sensitive(devices.iter().any(|x| x.1.get_active()));
+                        }
+                    }
+                    gtk::Continue(true)
+                } else {
+                    gtk::Continue(false)
+                }
+            });
+        }
 
         next.connect_clicked(move |next| {
-            let device_list = &state.devices;
             state.buffer.state.store(image::SLEEPING, Ordering::SeqCst);
-            let (_, ref mut image_data) = *try_or_error!(
-                state.buffer.data.lock(),
-                state.view,
-                back,
-                next,
-                stack,
-                error,
-                "mutex lock failure",
-                ()
-            );
-            let start = &state.start;
-            let task_handles = &state.task_handles;
-            let bars = &state.bars;
-            let tasks = &state.tasks;
+
             let view = &state.view;
             let view_value = view.get();
             stack.set_transition_type(StackTransitionType::SlideLeft);
 
             match view_value {
-                // Move to device selection screen
-                0 => {
-                    back.set_label("Back");
-                    back.get_style_context().map(|c| {
-                        c.add_class("back-button");
-                    });
-                    next.set_label("Flash");
-                    next.get_style_context().map(|c| {
-                        c.remove_class("suggested-action");
-                        c.add_class("destructive-action");
-                    });
-                    stack.set_visible_child_name("devices");
-
-                    // Remove all but the first row
-                    list.get_children()
-                        .into_iter()
-                        .for_each(|widget| widget.destroy());
-
-                    let mut devices = vec![];
-                    if let Err(why) = popsicle::get_disk_args(&mut devices) {
-                        eprintln!("popsicle: unable to get devices: {}", why);
-                    }
-
-                    let mut device_list = try_or_error!(
-                        device_list.lock(),
-                        state.view,
-                        back,
-                        next,
-                        stack,
-                        error,
-                        "device list mutex lock failure",
-                        ()
-                    );
-                    device_list.clear();
-                    for device in &devices {
-                        // Attempt to get the canonical path of the device.
-                        // Display the error view if this fails.
-                        let name = try_or_error!(
-                            Path::new(&device).canonicalize(),
-                            state.view,
-                            back,
-                            next,
-                            stack,
-                            error,
-                            format!("unable to get canonical path of '{}'", device),
-                            ()
-                        );
-
-                        let button = if let Some(block) = BlockDevice::new(&name) {
-                            CheckButton::new_with_label(&[
-                                &block.label(),
-                                " (",
-                                &name.to_string_lossy(),
-                                ")",
-                            ].concat())
-                        } else {
-                            CheckButton::new_with_label(&name.to_string_lossy())
-                        };
-
-                        list.insert(&button, -1);
-                        device_list.push((device.clone(), button));
-                    }
-
-                    list.show_all();
-                }
-                // Begin the device flashing process
-                1 => {
-                    back.get_style_context().map(|c| {
-                        c.remove_class("back-button");
-                    });
-
-                    let device_list = try_or_error!(
-                        device_list.lock(),
-                        state.view,
-                        back,
-                        next,
-                        stack,
-                        error,
-                        "device list mutex lock failure",
-                        ()
-                    );
-                    let devs = device_list
-                        .iter()
-                        .filter(|x| x.1.get_active())
-                        .map(|x| x.0.clone());
-
-                    let mounts = try_or_error!(
-                        popsicle::Mount::all(),
-                        state.view,
-                        back,
-                        next,
-                        stack,
-                        error,
-                        "unable to obtain mount points",
-                        ()
-                    );
-
-                    let disks = try_or_error!(
-                        popsicle::disks_from_args(devs, &mounts, true),
-                        state.view,
-                        back,
-                        next,
-                        stack,
-                        error,
-                        "unable to get collect devices",
-                        ()
-                    );
-
-                    back.set_visible(false);
-                    next.set_visible(false);
-                    stack.set_visible_child_name("flash");
-
-                    // Clear the progress bar summaries.
-                    let mut bars = bars.borrow_mut();
-                    bars.clear();
-                    summary_grid.get_children().iter().for_each(|c| c.destroy());
-
-                    *start.borrow_mut() = Instant::now();
-                    let mut tasks = try_or_error!(
-                        tasks.lock(),
-                        state.view,
-                        back,
-                        next,
-                        stack,
-                        error,
-                        "tasks mutex lock failure",
-                        ()
-                    );
-
-                    let mut task_handles = try_or_error!(
-                        task_handles.lock(),
-                        state.view,
-                        back,
-                        next,
-                        stack,
-                        error,
-                        "task handles mutex lock failure",
-                        ()
-                    );
-
-                    // Take ownership of the data, so that we may wrap it within an `Arc`
-                    // and redistribute it across threads.
-                    //
-                    // Note: Possible optimization could be done to avoid the wrap.
-                    //       Avoiding the wrap could eliminate two allocations.
-                    let mut data = Vec::new();
-                    mem::swap(&mut data, image_data);
-                    let image_data = Arc::new(data);
-
-                    for (id, (disk_path, mut disk)) in disks.into_iter().enumerate() {
-                        let id = id as i32;
-                        let image_data = image_data.clone();
-                        let progress = Arc::new(AtomicUsize::new(0));
-                        let finished = Arc::new(AtomicUsize::new(0));
-                        let bar = ProgressBar::new();
-                        bar.set_hexpand(true);
-
-                        let label = {
-                            let disk_path = try_or_error!(
-                                Path::new(&disk_path).canonicalize(),
-                                state.view,
-                                back,
-                                next,
-                                stack,
-                                error,
-                                format!("unable to get canonical path of {}", disk_path),
-                                ()
-                            );
-                            if let Some(block) = BlockDevice::new(&disk_path) {
-                                Label::new(
-                                    [&block.label(), " (", &disk_path.to_string_lossy(), ")"]
-                                        .concat()
-                                        .as_str(),
-                                )
-                            } else {
-                                Label::new(disk_path.to_string_lossy().as_ref())
-                            }
-                        };
-
-                        label.set_justify(Justification::Right);
-                        label.get_style_context().map(|c| c.add_class("bold"));
-                        let bar_label = Label::new("");
-                        bar_label.set_halign(Align::Center);
-                        let bar_container = Box::new(Orientation::Vertical, 0);
-                        bar_container.pack_start(&bar, false, false, 0);
-                        bar_container.pack_start(&bar_label, false, false, 0);
-                        summary_grid.attach(&label, 0, id, 1, 1);
-                        summary_grid.attach(&bar_container, 1, id, 1, 1);
-                        bars.push((bar, bar_label));
-
-                        // Spawn a thread that will update the progress value over time.
-                        //
-                        // The value will be stored within an intermediary atomic integer,
-                        // because it is unsafe to send GTK widgets across threads.
-                        task_handles.push({
-                            let progress = progress.clone();
-                            let finished = finished.clone();
-                            thread::spawn(move || -> Result<(), DiskError> {
-                                let result = popsicle::write_to_disk(
-                                    |_msg| (),
-                                    || (),
-                                    |value| progress.store(value as usize, Ordering::SeqCst),
-                                    disk,
-                                    disk_path,
-                                    image_data.len() as u64,
-                                    &image_data,
-                                    false,
-                                );
-
-                                finished.store(1, Ordering::SeqCst);
-                                result
-                            })
-                        });
-
-                        tasks.push(FlashTask {
-                            previous: Arc::new(Mutex::new([0; 7])),
-                            progress,
-                            finished,
-                        });
-                    }
-
-                    summary_grid.show_all();
-                }
+                0 => device_selection::initialize(&state, &back, &error, &list, &next, &stack),
+                1 => flash_devices(&state, &back, &error, &next, &stack, &summary_grid),
                 2 => gtk::main_quit(),
                 _ => unreachable!(),
             }
 
             view.set(view_value + 1);
+
+            if view.get() == 1 {
+                watch_device_selection(
+                    state.clone(),
+                    all.clone(),
+                    back.clone(),
+                    error.clone(),
+                    list.clone(),
+                    next.clone(),
+                    stack.clone()
+                );
+            }
         });
     }
 
@@ -621,6 +414,7 @@ impl Connect for App {
             }
 
             if finished {
+                back.set_visible(false);
                 stack.set_visible_child_name("summary");
                 next.set_label("Close");
                 next.get_style_context()
@@ -673,6 +467,7 @@ impl Connect for App {
 
                 if errored.is_empty() {
                     description.set_text(&format!("{} devices successfully flashed", ntasks));
+                    list.set_visible(false);
                 } else {
                     description.set_text(&format!(
                         "{} of {} devices successfully flashed",
diff --git a/gtk/src/app/state/try.rs b/gtk/src/app/state/try.rs
new file mode 100644
index 0000000..10dd0fe
--- /dev/null
+++ b/gtk/src/app/state/try.rs
@@ -0,0 +1,30 @@
+#[macro_export]
+macro_rules! try_or_error {
+    (
+        $act:expr,
+        $view:expr,
+        $back:expr,
+        $next:expr,
+        $stack:ident,
+        $error:ident,
+        $msg:expr,
+        $val:expr
+    ) => {
+        match $act {
+            Ok(value) => value,
+            Err(why) => {
+                $back.set_visible(false);
+                $next.set_visible(true);
+                $next.set_label("Close");
+                $next.get_style_context().map(|c| {
+                    c.remove_class("destructive-action");
+                    c.remove_class("suggested-action");
+                });
+                $error.set_text(&format!("{}: {:?}", $msg, why));
+                $view.set(2);
+                $stack.set_visible_child_name("error");
+                return $val;
+            }
+        }
+    };
+}


Test patch: