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

Issue:
Feature request: Add ability to continue flashing different images after first one is complete
<!--

If this is a bug, please use the template below. If this is a question or general discussion topic, please start a conversation in our chat https://chat.pop-os.org/ or post on our subreddit https://reddit.com/r/pop_os - as those are the proper forums for that type of discussion.

-->


**Distribution (run `cat /etc/os-release`):**
 Pop!_OS 18.04
 
 
**Related Application and/or Package Version (run `apt policy $PACKAGE NAME`):**
popsicle-gtk


**Issue/Bug Description:**
As someone who uses popsicle quite a lot to image a number of different images, after writing an image to an external drive, I currently have to close the application and then relaunch it to image another drive with a different image. 

I can imagine a button in the top left (opposite from the "Close" button) that would go back to the "choose an image" page.


Discussion / hints:
I'll have a PR submitted tomorrow morning. I have it working at the moment, but I would like to improve my current implementation before I submit it.

Fix patch:
diff --git a/Cargo.toml b/Cargo.toml
index 5cce485..f0af4a8 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -2,7 +2,10 @@
 name = "popsicle"
 description = "USB Flasher"
 version = "0.1.5"
-authors = ["Jeremy Soller <jeremy@system76.com>"]
+authors = [
+    "Jeremy Soller <jeremy@system76.com>",
+    "Michael Aaron Murphy <michael@system76.com>",
+]
 license = "MIT"
 readme = "README.md"
 
diff --git a/README.md b/README.md
index 58ab17e..1b2c239 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,6 @@
 
 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
 
@@ -17,3 +16,26 @@ 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
+
+## Screenshots
+
+### Image Selection
+
+![Image Selection](./screenshots/screenshot-01.png)
+
+### Device Selection
+
+![Device Selection](./screenshots/screenshot-02.png)
+
+The list will also dynamically refresh as devices are added and removed
+
+![GIF Demo](./screenshots/device-monitoring.gif)
+
+### Device Flashing
+
+![Flashing Devices](./screenshots/screenshot-03.png)
+![Flashing Devices](./screenshots/screenshot-04.png)
+
+### Summary
+
+![Summary](./screenshots/screenshot-05.png)
\ No newline at end of file
diff --git a/cli/src/main.rs b/cli/src/main.rs
index dc451a6..eb24932 100644
--- a/cli/src/main.rs
+++ b/cli/src/main.rs
@@ -157,6 +157,7 @@ fn popsicle() -> Result<(), String> {
                         pb.borrow_mut().finish();
                     }
                 },
+                || false,
                 disk,
                 &disk_path,
                 image_size,
diff --git a/demo.gif b/demo.gif
deleted file mode 100644
index 05119e7..0000000
Binary files a/demo.gif and /dev/null differ
diff --git a/gtk/Cargo.toml b/gtk/Cargo.toml
index 1463c14..adf165b 100644
--- a/gtk/Cargo.toml
+++ b/gtk/Cargo.toml
@@ -2,7 +2,7 @@
 name = "popsicle_gtk"
 description = "USB Flasher"
 version = "0.1.5"
-authors = ["Jeremy Soller <jeremy@system76.com>"]
+authors = [ "Michael Aaron Murphy <michael@system76.com>" ]
 license = "MIT"
 readme = "README.md"
 
diff --git a/gtk/src/app/content/devices.rs b/gtk/src/app/content/devices.rs
index a87fc68..3dbdf72 100644
--- a/gtk/src/app/content/devices.rs
+++ b/gtk/src/app/content/devices.rs
@@ -1,24 +1,29 @@
 use super::View;
-use gtk::*;
+use gtk::prelude::*;
+use gtk;
+use popsicle;
+use block::BlockDevice;
+use std::path::Path;
+use app::state::State;
+use std::sync::Arc;
 
 pub struct DevicesView {
-    pub view:       View,
-    pub list:       ListBox,
-    pub select_all: CheckButton,
+    pub view: View,
+    pub list: DeviceList,
 }
 
 impl DevicesView {
     pub fn new() -> DevicesView {
-        let select_all = CheckButton::new_with_label("Select all");
-        let list = ListBox::new();
+        let select_all = gtk::CheckButton::new_with_label("Select all");
+        let list = gtk::ListBox::new();
         list.get_style_context().map(|c| c.add_class("devices"));
 
-        let list_box = Box::new(Orientation::Vertical, 0);
+        let list_box = gtk::Box::new(gtk::Orientation::Vertical, 0);
         list_box.pack_start(&select_all, false, false, 0);
-        list_box.pack_start(&Separator::new(Orientation::Horizontal), false, false, 0);
+        list_box.pack_start(&gtk::Separator::new(gtk::Orientation::Horizontal), false, false, 0);
         list_box.pack_start(&list, true, true, 0);
 
-        let select_scroller = ScrolledWindow::new(None, None);
+        let select_scroller = gtk::ScrolledWindow::new(None, None);
         select_scroller.add(&list_box);
 
         let view = View::new(
@@ -30,8 +35,107 @@ impl DevicesView {
 
         DevicesView {
             view,
-            list,
-            select_all,
+            list: DeviceList {
+                list,
+                select_all,
+            }
+        }
+    }
+}
+
+pub struct DeviceList {
+    pub list:       gtk::ListBox,
+    pub select_all: gtk::CheckButton,
+}
+
+impl DeviceList {
+    pub fn clear(&self) {
+        self.list.get_children()
+            .into_iter()
+            .for_each(|widget| widget.destroy());
+    }
+
+    pub fn connect_select_all<F>(&self, state: Arc<State>, result: F )
+        where F: 'static + Fn(Result<(), String>)
+    {
+        self.select_all.connect_clicked(move |all| result(
+            state.devices.lock()
+                .map_err(|why| format!("devices mutex lock failed: {}", why))
+                .map(|ref devices| {
+                    devices.iter()
+                        .for_each(|&(_, ref device)| device.set_active(
+                            all.get_active() && device.is_sensitive()
+                        ))
+                }
+            )
+        ));
+    }
+
+    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())
+    }
+
+    pub fn refresh(
+        &self,
+        device_list: &mut Vec<(String, gtk::CheckButton)>,
+        devices: &[String],
+        image_sectors: u64,
+    ) -> Result<(), String> {
+        device_list.clear();
+        self.clear();
+
+        let mut all_is_sensitive = false;
+        for device in devices {
+            // Attempt to get the canonical path of the device.
+            // Display the error view if this fails.
+            let name = Path::new(&device).canonicalize()
+                .map_err(|why| format!("unable to get canonical path of '{}': {}", device, why))?;
+
+            let button = if let Some(block) = BlockDevice::new(&name) {
+                let too_small = block.sectors() < image_sectors;
+
+                let button = gtk::CheckButton::new_with_label(&{
+                    if too_small {
+                        [ &block.label(), " (", &name.to_string_lossy(), "): Device is too small" ].concat()
+                    } else {
+                        [ &block.label(), " (", &name.to_string_lossy(), ")" ].concat()
+                    }
+                });
+
+                if too_small {
+                    button.set_tooltip_text("Device is too small");
+                    button.set_has_tooltip(true);
+                    button.set_sensitive(false);
+                } else {
+                    all_is_sensitive = true;
+                }
+                button
+            } else {
+                gtk::CheckButton::new_with_label(&name.to_string_lossy())
+            };
+
+            self.list.insert(&button, -1);
+            device_list.push((device.clone(), button));
+        }
+
+        self.list.show_all();
+        self.select_all.set_sensitive(all_is_sensitive);
+        Ok(())
+    }
+
+    pub fn requires_refresh(device_list: &[(String, gtk::CheckButton)]) -> Option<Vec<String>> {
+        let mut devices = vec![];
+        if let Err(why) = popsicle::get_disk_args(&mut devices) {
+            eprintln!("popsicle: unable to get devices: {}", why);
+            return None;
+        }
+
+        if devices.len() != device_list.len() || Self::devices_differ(&devices, &device_list) {
+            Some(devices)
+        } else {
+            None
         }
     }
 }
diff --git a/gtk/src/app/content/mod.rs b/gtk/src/app/content/mod.rs
index 7b51ff1..567d8e5 100644
--- a/gtk/src/app/content/mod.rs
+++ b/gtk/src/app/content/mod.rs
@@ -5,7 +5,7 @@ mod images;
 mod summary;
 mod view;
 
-pub use self::devices::DevicesView;
+pub use self::devices::{DeviceList, DevicesView};
 pub use self::error::ErrorView;
 pub use self::flashing::FlashView;
 pub use self::images::ImageView;
diff --git a/gtk/src/app/mod.rs b/gtk/src/app/mod.rs
index 9ba0b74..11926fe 100644
--- a/gtk/src/app/mod.rs
+++ b/gtk/src/app/mod.rs
@@ -4,24 +4,31 @@ mod header;
 mod misc;
 mod state;
 
-use self::content::Content;
+use self::content::{Content, DeviceList};
 pub use self::dialogs::OpenDialog;
 use self::header::Header;
 pub use self::misc::*;
-pub use self::state::{Connect, State};
+pub use self::state::{Connect, FlashTask, State};
 
 // TODO: Use AtomicU64 / Bool when https://github.com/rust-lang/rust/issues/32976 is stable.
 
-use std::path::PathBuf;
+use std::mem;
+use std::ops::{Deref, DerefMut};
+use std::path::{Path, PathBuf};
 use std::process;
-use std::sync::Arc;
+use std::rc::Rc;
+use std::sync::{Arc, Mutex};
+use std::sync::atomic::{AtomicUsize, Ordering};
 use std::sync::mpsc::{Sender, Receiver};
+use std::thread;
+use std::time::{Duration, Instant};
 use std::fs::File;
 use std::thread::JoinHandle;
 
+use block::BlockDevice;
 use flash::FlashRequest;
-use popsicle::mnt::MountEntry;
-use popsicle::DiskError;
+use popsicle::mnt::{self, MountEntry};
+use popsicle::{self, DiskError};
 
 use gtk;
 use gtk::*;
@@ -29,9 +36,7 @@ use gtk::*;
 const CSS: &str = include_str!("ui.css");
 
 pub struct App {
-    pub window:  Window,
-    pub header:  Header,
-    pub content: Content,
+    pub widgets: Rc<AppWidgets>,
     pub state:   Arc<State>,
 }
 
@@ -83,10 +88,396 @@ impl App {
 
         // Return the application structure.
         App {
-            window,
-            header,
-            content,
+            widgets: Rc::new(AppWidgets { window, header, content }),
             state: Arc::new(State::new(sender, devices_request, devices_response, flash_request, flash_response)),
         }
     }
 }
+
+pub struct AppWidgets {
+    pub window:  Window,
+    pub header:  Header,
+    pub content: Content,
+}
+
+impl AppWidgets {
+    pub fn switch_to_main(&self, state: &State) {
+        let stack = &self.content.container;
+        let back = &self.header.back;
+        let next = &self.header.next;
+
+        // If tasks are running, signify that tasks should be considered as completed.
+        if 1 == state.flash_state.load(Ordering::SeqCst) {
+            state.flash_state.store(2, Ordering::SeqCst);
+        }
+
+        self.content.devices_view.list.select_all.set_active(false);
+
+        stack.set_transition_type(StackTransitionType::SlideRight);
+        stack.set_visible_child_name("image");
+
+        back.set_visible(true);
+        back.set_label("Cancel");
+        back.get_style_context().map(|c| {
+            c.remove_class("back-button");
+            c.remove_class("destructive-action");
+        });
+
+        next.set_visible(true);
+        next.set_label("Next");
+        next.set_sensitive(true);
+        next.get_style_context().map(|c| {
+            c.remove_class("destructive-action");
+            c.add_class("suggested-action");
+        });
+
+        state.view.set(0)
+    }
+
+    pub fn switch_to_device_selection(&self, state: &State) {
+        let stack = &self.content.container;
+        let back = &self.header.back;
+        let next = &self.header.next;
+        let list = &self.content.devices_view.list;
+
+        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");
+
+        let image_sectors = (state.image_length.get() / 512 + 1) as u64;
+        let mut devices = vec![];
+        if let Err(why) = popsicle::get_disk_args(&mut devices) {
+            eprintln!("popsicle: unable to get devices: {}", why);
+        }
+
+        if let Err(why) = state.devices.lock()
+            .map_err(|why| format!("mutex lock failed: {}", why))
+            .and_then(|ref mut device_list| {
+                list.refresh(device_list, &devices, image_sectors)
+            })
+        {
+            self.set_error(state, &why);
+        }
+    }
+
+    pub fn watch_device_selection(widgets: Rc<AppWidgets>, state: Arc<State>) {
+        gtk::timeout_add(16, move || {
+            let list = &widgets.content.devices_view.list;
+            let next = &widgets.header.next;
+
+            if state.view.get() != 1 {
+                return gtk::Continue(false);
+            }
+
+            let mut disable_select_all = false;
+
+            if let Ok(ref mut device_list) = state.devices.try_lock() {
+                let mut check_refresh = || -> Result<(), String> {
+                    match DeviceList::requires_refresh(&device_list) {
+                        Some(devices) => {
+                            let image_sectors = (state.image_length.get() / 512 + 1) as u64;
+                            list.refresh(device_list, &devices, image_sectors)?;
+                            disable_select_all = true;
+                            next.set_sensitive(false);
+                        }
+                        None => {
+                            next.set_sensitive(device_list.iter().any(|x| x.1.get_active()));
+                        }
+                    }
+
+                    Ok(())
+                };
+
+                if let Err(why) = check_refresh() {
+                    widgets.set_error(&state, &why);
+                }
+            }
+
+            if disable_select_all {
+                list.select_all.set_active(false);
+            }
+
+            gtk::Continue(true)
+        });
+    }
+
+    pub fn switch_to_device_flashing(&self, state: &State) {
+        let back = &self.header.back;
+        let next = &self.header.next;
+        let stack = &self.content.container;
+        let summary_grid = &self.content.flash_view.progress_list;
+        let task_handles = &state.task_handles;
+        let bars = &state.bars;
+        let start = &state.start;
+        let tasks = &state.tasks;
+
+        macro_rules! try_or_error {
+            ($action:expr, $msg:expr) => {{
+                match $action {
+                    Ok(value) => value,
+                    Err(why) => {
+                        self.set_error(state, &format!("{}: {}", $msg, why));
+                        return;
+                    }
+                }
+            }}
+        }
+
+        // Wait for the flash state to be 0 before proceeding.
+        while state.flash_state.load(Ordering::SeqCst) != 0 {
+            thread::sleep(Duration::from_millis(16));
+        }
+
+        let mut data = Vec::new();
+        let image_data: Arc<Vec<u8>>;
+
+        {
+            let mut image_buffer_lock = try_or_error!(
+                state.buffer.data.lock(),
+                "failed to lock buffer.data mutex"
+            );
+
+            let device_list = try_or_error!(
+                state.devices.lock(),
+                "device list mutex lock failure"
+            );
+
+            let devs = device_list
+                .iter()
+                .filter(|x| x.1.get_active())
+                .map(|x| x.0.clone())
+                .collect::<Vec<_>>();
+
+            let mounts = try_or_error!(
+                mnt::get_submounts(Path::new("/")),
+                "unable to obtain mount points"
+            );
+
+            try_or_error!(
+                state.devices_request.send((devs, mounts.clone())),
+                "unable to send device request"
+            );
+
+            let disks_result = try_or_error!(
+                state.devices_response.recv(),
+                "unable to get device request response"
+            );
+
+            let disks = try_or_error!(
+                disks_result,
+                "unable to get 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(),
+                "tasks mutex lock failure"
+            );
+
+            let mut task_handles = try_or_error!(
+                task_handles.lock(),
+                "task handles mutex lock failure"
+            );
+
+            state.flash_state.store(1, Ordering::SeqCst);
+
+            // 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.
+            image_data = {
+                let (_, ref mut image_data) = *image_buffer_lock;
+                mem::swap(&mut data, 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 pbar = ProgressBar::new();
+                pbar.set_hexpand(true);
+
+                let label = {
+                    let disk_path = try_or_error!(
+                        Path::new(&disk_path).canonicalize(),
+                        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(&pbar, 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((pbar, 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 _ = state.flash_request.send(FlashRequest::new(
+                        disk,
+                        disk_path,
+                        image_data.len() as u64,
+                        image_data,
+                        state.flash_state.clone(),
+                        progress.clone(),
+                        finished.clone()
+                    ));
+
+                    state.flash_response.recv().expect("expected join handle to be returned")
+                });
+
+                tasks.push(FlashTask {
+                    previous: Arc::new(Mutex::new([0; 7])),
+                    progress,
+                    finished,
+                });
+            }
+        }
+
+        state.async_reattain_image_data(image_data);
+        summary_grid.show_all();
+    }
+
+    pub fn switch_to_summary(&self, state: &State, ntasks: usize) -> Result<(), ()> {
+        let stack = &self.content.container;
+        let back = &self.header.back;
+        let next = &self.header.next;
+        let description = &self.content.summary_view.view.description.clone();
+        let list = &self.content.summary_view.list.clone();
+        let devices = &state.devices;
+        let task_handles = &state.task_handles;
+
+        back.set_label("Flash Again");
+        back.get_style_context()
+            .map(|c| c.remove_class("destructive-action"));
+        next.set_label("Done");
+        next.get_style_context()
+            .map(|c| c.remove_class("destructive-action"));
+        next.set_visible(true);
+        stack.set_visible_child_name("summary");
+
+        macro_rules! try_or_error {
+            ($action:expr, $msg:expr) => {{
+                match $action {
+                    Ok(value) => value,
+                    Err(why) => {
+                        self.set_error(state, &format!("{}: {:?}", $msg, why));
+                        return Err(());
+                    }
+                }
+            }}
+        }
+
+        let mut errored: Vec<(String, DiskError)> = Vec::new();
+        let mut task_handles = try_or_error!(
+            task_handles.lock(),
+            "task handles mutex lock failure"
+        );
+
+        let devices = try_or_error!(
+            devices.lock(),
+            "devices mutex lock failure"
+        );
+
+        let handle_iter = task_handles.deref_mut().drain(..);
+        let mut device_iter = devices.deref().iter();
+        for handle in handle_iter {
+            if let Some(&(ref device, _)) = device_iter.next() {
+                let result = try_or_error!(
+                    handle.join(),
+                    "thread handle join failure"
+                );
+
+                if let Err(why) = result {
+                    errored.push((device.clone(), why));
+                }
+            }
+        }
+
+        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",
+                ntasks - errored.len(),
+                ntasks
+            ));
+            list.set_visible(true);
+            for (device, why) in errored {
+                let container = Box::new(Orientation::Horizontal, 0);
+                let device = Label::new(device.as_str());
+                let why = Label::new(format!("{}", why).as_str());
+                container.pack_start(&device, false, false, 0);
+                container.pack_start(&why, true, true, 0);
+                list.insert(&container, -1);
+            }
+        }
+
+        state.set_as_complete();
+
+        Ok(())
+    }
+
+    pub fn set_error(&self, state: &State, msg: &str) {
+        let stack = &self.content.container;
+        let back = &self.header.back;
+        let next = &self.header.next;
+        let error = &self.content.error_view.view.description;
+
+        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(&msg);
+        state.view.set(2);
+        stack.set_visible_child_name("error");
+
+        state.set_as_complete();
+        state.reset_after_reacquiring_image();
+    }
+}
diff --git a/gtk/src/app/state/device_selection.rs b/gtk/src/app/state/device_selection.rs
deleted file mode 100644
index 4ae40e6..0000000
--- a/gtk/src/app/state/device_selection.rs
+++ /dev/null
@@ -1,176 +0,0 @@
-
-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,
-    all: &gtk::CheckButton,
-    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, all, back, error, list, next, stack);
-}
-
-pub fn refresh_device_list(
-    state: &State,
-    devices: &[String],
-    all: &gtk::CheckButton,
-    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());
-    let image_sectors = (state.image_length.get() / 512 + 1) as u64;
-    let mut all_is_sensitive = false;
-    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) {
-            let too_small = block.sectors() < image_sectors;
-
-            let button = CheckButton::new_with_label(&{
-                if too_small {
-                    [ &block.label(), " (", &name.to_string_lossy(), "): Device is too small" ].concat()
-                } else {
-                    [ &block.label(), " (", &name.to_string_lossy(), ")" ].concat()
-                }
-            });
-
-            if too_small {
-                button.set_tooltip_text("Device is too small");
-                button.set_has_tooltip(true);
-                button.set_sensitive(false);
-            } else {
-                all_is_sensitive = true;
-            }
-            button
-        } else {
-            CheckButton::new_with_label(&name.to_string_lossy())
-        };
-
-        list.insert(&button, -1);
-        device_list.push((device.clone(), button));
-    }
-
-    list.show_all();
-    all.set_sensitive(all_is_sensitive);
-}
-
-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
deleted file mode 100644
index 0a33e4f..0000000
--- a/gtk/src/app/state/flash_devices.rs
+++ /dev/null
@@ -1,238 +0,0 @@
-use super::*;
-use gtk;
-use gtk::prelude::*;
-use std::sync::{Arc, Mutex};
-use popsicle::mnt;
-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())
-        .collect::<Vec<_>>();
-
-    let mounts = try_or_error!(
-        mnt::get_submounts(Path::new("/")),
-        state.view,
-        back,
-        next,
-        stack,
-        error,
-        "unable to obtain mount points",
-        ()
-    );
-
-    try_or_error!(
-        state.devices_request.send((devs, mounts.clone())),
-        state.view,
-        back,
-        next,
-        stack,
-        error,
-        "unable to send device request",
-        ()
-    );
-
-    let disks_result = try_or_error!(
-        state.devices_response.recv(),
-        state.view,
-        back,
-        next,
-        stack,
-        error,
-        "unable to get device request response",
-        ()
-    );
-
-    let disks = try_or_error!(
-        disks_result,
-        state.view,
-        back,
-        next,
-        stack,
-        error,
-        "unable to get 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 pbar = ProgressBar::new();
-        pbar.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(&pbar, 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((pbar, 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 _ = state.flash_request.send(FlashRequest::new(
-                disk,
-                disk_path,
-                image_data.len() as u64,
-                image_data,
-                progress.clone(),
-                finished.clone()
-            ));
-
-            state.flash_response.recv().expect("expected join handle to be returned")
-        });
-
-        tasks.push(FlashTask {
-            previous: Arc::new(Mutex::new([0; 7])),
-            progress,
-            finished,
-        });
-    }
-
-    summary_grid.show_all();
-}
diff --git a/gtk/src/app/state/mod.rs b/gtk/src/app/state/mod.rs
index 344e6fd..14bb347 100644
--- a/gtk/src/app/state/mod.rs
+++ b/gtk/src/app/state/mod.rs
@@ -1,21 +1,18 @@
-mod device_selection;
-mod flash_devices;
 #[macro_use]
 mod try;
 
-use self::flash_devices::flash_devices;
 use flash::FlashRequest;
 
-use super::super::BlockDevice;
 use super::{App, OpenDialog};
+use app::AppWidgets;
 
 use hash::HashState;
 use image::{self, BufferingData};
 use std::fs::File;
 use std::cell::{Cell, RefCell};
 use std::mem;
-use std::ops::{Deref, DerefMut};
-use std::path::{Path, PathBuf};
+use std::ops::Deref;
+use std::path::PathBuf;
 use std::sync::atomic::{AtomicUsize, Ordering};
 use std::sync::mpsc::{Sender, Receiver};
 use std::sync::{Arc, Mutex};
@@ -59,6 +56,8 @@ pub struct State {
     pub flash_request: Sender<FlashRequest>,
     /// Contains the join handle to the thread where the task is being flashed.
     pub flash_response: Receiver<JoinHandle<Result<(), DiskError>>>,
+    /// Signifies the status of flashing
+    pub flash_state: Arc<AtomicUsize>,
 }
 
 impl State {
@@ -73,11 +72,12 @@ impl State {
         State {
             bars: RefCell::new(Vec::new()),
             devices: Mutex::new(Vec::new()),
-            hash: Arc::new(HashState::new()),
             task_handles: Mutex::new(Vec::new()),
             tasks: Mutex::new(Vec::new()),
             view: Cell::new(0),
             start: RefCell::new(unsafe { mem::uninitialized() }),
+            flash_state: Arc::new(AtomicUsize::new(0)),
+            hash: Arc::new(HashState::new()),
             buffer: Arc::new(BufferingData::new()),
             image_sender,
             image_length: Cell::new(0),
@@ -87,12 +87,77 @@ impl State {
             flash_response,
         }
     }
+
+    pub fn set_as_complete(&self) {
+        self.flash_state.store(2, Ordering::SeqCst);
+        while 3 != self.flash_state.load(Ordering::SeqCst) {
+            thread::sleep(Duration::from_millis(16));
+        }
+    }
+
+    pub fn reset_after_reacquiring_image(&self) {
+        if 3 == self.flash_state.load(Ordering::SeqCst) {
+            self.flash_state.store(4, Ordering::SeqCst);
+            while 5 != self.flash_state.load(Ordering::SeqCst) {
+                thread::sleep(Duration::from_millis(16));
+            }
+            self.reset();
+        }
+    }
+
+    fn reset(&self) {
+        self.bars.borrow_mut().clear();
+        if let Ok(ref mut devices) = self.devices.lock() {
+            devices.clear();
+        }
+
+        if let Ok(ref mut handles) = self.task_handles.lock() {
+            if !handles.is_empty() {
+                handles.drain(..).for_each(|x| { let _ = x.join(); });
+            }
+        }
+
+        if let Ok(ref mut tasks) = self.tasks.lock() {
+            tasks.clear();
+        }
+
+        self.flash_state.store(0, Ordering::SeqCst);
+    }
+
+    pub fn async_reattain_image_data(&self, image_data: Arc<Vec<u8>>) {
+        let buffer = self.buffer.clone();
+        let flash_state = self.flash_state.clone();
+        thread::spawn(move || loop {
+            if flash_state.load(Ordering::SeqCst) == 2 {
+                flash_state.store(3, Ordering::SeqCst);
+
+                // Wait for the main GTK event loop to sleep so that we have exclusive state lock access.
+                while 4 != flash_state.load(Ordering::SeqCst) {
+                    thread::sleep(Duration::from_millis(16))
+                }
+
+                // This will be 1 once the device flashing threads have exited.
+                while 1 != Arc::strong_count(&image_data) {
+                    thread::sleep(Duration::from_millis(16));
+                }
+
+                let (_, ref mut data) = *buffer.data.lock().expect("failed to get lock on buffer.data");
+                let mut replace_with = Arc::try_unwrap(image_data).expect("image_data is still shared");
+                mem::swap(data, &mut replace_with);
+
+                flash_state.store(5, Ordering::SeqCst);
+                break
+            }
+
+            thread::sleep(Duration::from_millis(16));
+        });
+    }
 }
 
 pub struct FlashTask {
-    progress: Arc<AtomicUsize>,
-    previous: Arc<Mutex<[usize; 7]>>,
-    finished: Arc<AtomicUsize>,
+    pub progress: Arc<AtomicUsize>,
+    pub previous: Arc<Mutex<[usize; 7]>>,
+    pub finished: Arc<AtomicUsize>,
 }
 
 pub struct Connected(App);
@@ -100,7 +165,7 @@ pub struct Connected(App);
 impl Connected {
     /// Display the window, and execute the gtk main event loop.
     pub fn then_execute(self) {
-        self.0.window.show_all();
+        self.0.widgets.window.show_all();
         gtk::main();
     }
 }
@@ -144,7 +209,7 @@ impl Connect for App {
 
     fn connect_image_chooser(&self) {
         let state = self.state.clone();
-        self.content.image_view.chooser.connect_clicked(move |_| {
+        self.widgets.content.image_view.chooser.connect_clicked(move |_| {
             if let Some(path) = OpenDialog::new(None).run() {
                 let _ = state.image_sender.send(path);
             }
@@ -153,8 +218,8 @@ impl Connect for App {
 
     fn connect_hash_generator(&self) {
         let state = self.state.clone();
-        let hash_label = self.content.image_view.hash_label.clone();
-        self.content
+        let hash_label = self.widgets.content.image_view.hash_label.clone();
+        self.widgets.content
             .image_view
             .hash
             .connect_changed(move |hash_kind| {
@@ -191,83 +256,27 @@ impl Connect for App {
     }
 
     fn connect_back_button(&self) {
-        let stack = self.content.container.clone();
-        let back = self.header.back.clone();
-        let next = self.header.next.clone();
+        let widgets = self.widgets.clone();
         let state = self.state.clone();
-        back.connect_clicked(move |back| {
-            let view = state.view.get();
-            match view {
+        self.widgets.header.back.connect_clicked(move |_| {
+            match state.view.get() {
                 0 => {
                     gtk::main_quit();
                     return;
                 },
-                1 => {
-                    stack.set_transition_type(StackTransitionType::SlideRight);
-                    stack.set_visible_child_name("image");
-                    back.set_label("Cancel");
-                    back.get_style_context().map(|c| {
-                        c.remove_class("back-button");
-                    });
-                    next.set_label("Next");
-                    next.set_sensitive(true);
-                    next.get_style_context().map(|c| {
-                        c.remove_class("destructive-action");
-                        c.add_class("suggested-action");
-                    });
-                }
-                2 => {
-                    gtk::main_quit();
-                    return;
-                }
-                _ => unreachable!(),
+                _ => widgets.switch_to_main(&state),
             }
-
-            state.view.set(view - 1);
         });
     }
 
     fn connect_next_button(&self) {
         #[allow(unused_variables)]
-        let back = self.header.back.clone();
-        let list = self.content.devices_view.list.clone();
-        let back = self.header.back.clone();
-        let next = self.header.next.clone();
-        let stack = self.content.container.clone();
-        let summary_grid = self.content.flash_view.progress_list.clone();
+        let widgets = self.widgets.clone();
+        let next = widgets.header.next.clone();
+        let stack = widgets.content.container.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, &all, &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| {
+        next.connect_clicked(move |_| {
             state.buffer.state.store(image::SLEEPING, Ordering::SeqCst);
 
             let view = &state.view;
@@ -275,8 +284,8 @@ impl Connect for App {
             stack.set_transition_type(StackTransitionType::SlideLeft);
 
             match view_value {
-                0 => device_selection::initialize(&state, &all, &back, &error, &list, &next, &stack),
-                1 => flash_devices(&state, &back, &error, &next, &stack, &summary_grid),
+                0 => widgets.switch_to_device_selection(&state),
+                1 => widgets.switch_to_device_flashing(&state),
                 2 => gtk::main_quit(),
                 _ => unreachable!(),
             }
@@ -284,62 +293,46 @@ impl Connect for App {
             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()
-                );
+                AppWidgets::watch_device_selection(widgets.clone(), state.clone());
             }
         });
     }
 
     fn connect_check_all(&self) {
-        let all = self.content.devices_view.select_all.clone();
-        let back = self.header.back.clone();
-        let next = self.header.next.clone();
-        let stack = self.content.container.clone();
-        let error = self.content.error_view.view.description.clone();
         let state = self.state.clone();
-        all.connect_clicked(move |all| {
-            let devices = try_or_error!(
-                state.devices.lock(),
-                state.view,
-                back,
-                next,
-                stack,
-                error,
-                "devices mutex lock failure",
-                ()
-            );
-
-            devices
-                .iter()
-                .for_each(|&(_, ref device)| device.set_active(all.get_active() && device.is_sensitive()));
-        });
+        let widgets = self.widgets.clone();
+        widgets.clone().content.devices_view.list.connect_select_all(
+            state.clone(),
+            move |result| if let Err(why) = result {
+                widgets.set_error(&state, &format!("select all failed: {}", why));
+            }
+        )
     }
 
     fn watch_flashing_devices(&self) {
-        let stack = self.content.container.clone();
-        let back = self.header.back.clone();
-        let next = self.header.next.clone();
-        let description = self.content.summary_view.view.description.clone();
-        let list = self.content.summary_view.list.clone();
+        let next = self.widgets.header.next.clone();
         let state = self.state.clone();
-        let image_label = self.content.image_view.image_path.clone();
-        let chooser_container = self.content.image_view.chooser_container.clone();
-        let error = self.content.error_view.view.description.clone();
+        let image_label = self.widgets.content.image_view.image_path.clone();
+        let chooser_container = self.widgets.content.image_view.chooser_container.clone();
+        let widgets = self.widgets.clone();
 
         gtk::timeout_add(500, move || {
             let tasks = &state.tasks;
             let bars = &state.bars;
-            let devices = &state.devices;
-            let task_handles = &state.task_handles;
             let image_length = &state.image_length;
 
+            macro_rules! try_or_error {
+                ($action:expr, $msg:expr, $val:expr) => {{
+                    match $action {
+                        Ok(value) => value,
+                        Err(why) => {
+                            widgets.set_error(&state, &format!("{}: {:?}", $msg, why));
+                            return $val;
+                        }
+                    }
+                }}
+            }
+
             // Ensure that the image has been loaded before continuing.
             match state.buffer.state.load(Ordering::SeqCst) {
                 0 => {
@@ -359,11 +352,6 @@ impl Connect for App {
 
                     let (ref path, ref data) = *try_or_error!(
                         state.buffer.data.lock(),
-                        state.view,
-                        back,
-                        next,
-                        stack,
-                        error,
                         "image buffer mutex lock failure",
                         Continue(false)
                     );
@@ -383,134 +371,62 @@ impl Connect for App {
             }
 
             let image_length = image_length.get();
-
-            let tasks = try_or_error!(
-                tasks.lock(),
-                state.view,
-                back,
-                next,
-                stack,
-                error,
-                "tasks mutex lock failure",
-                Continue(false)
-            );
-
-            let ntasks = tasks.len();
-            if ntasks == 0 {
-                return Continue(true);
-            }
-
             let mut all_tasks_finished = true;
-            for (task, &(ref pbar, ref label)) in tasks.deref().iter().zip(bars.borrow().iter()) {
-                let raw_value = task.progress.load(Ordering::SeqCst);
-                let task_is_finished = task.finished.load(Ordering::SeqCst) == 1;
-                let value = if task_is_finished {
-                    1.0f64
-                } else {
-                    all_tasks_finished = false;
-                    raw_value as f64 / image_length as f64
-                };
+            let ntasks;
 
-                pbar.set_fraction(value);
-
-                if task_is_finished {
-                    label.set_label("Complete");
-                } else if let Ok(mut prev_values) = task.previous.lock() {
-                    prev_values[1] = prev_values[2];
-                    prev_values[2] = prev_values[3];
-                    prev_values[3] = prev_values[4];
-                    prev_values[4] = prev_values[5];
-                    prev_values[5] = prev_values[6];
-                    prev_values[6] = raw_value - prev_values[0];
-                    prev_values[0] = raw_value;
-
-                    let sum: usize = prev_values.iter().skip(1).sum();
-                    let per_second = sum / 3;
-                    label.set_label(&if per_second > (1024 * 1024) {
-                        format!("{} MiB/s", per_second / (1024 * 1024))
-                    } else {
-                        format!("{} KiB/s", per_second / 1024)
-                    });
-                }
-
-            }
-
-            if all_tasks_finished {
-                back.set_visible(false);
-                stack.set_visible_child_name("summary");
-                next.set_label("Close");
-                next.get_style_context()
-                    .map(|c| c.remove_class("destructive-action"));
-                next.set_visible(true);
-
-                let mut errored: Vec<(String, DiskError)> = Vec::new();
-                let mut task_handles = try_or_error!(
-                    task_handles.lock(),
-                    state.view,
-                    back,
-                    next,
-                    stack,
-                    error,
-                    "task handles mutex lock failure",
+            {
+                let tasks = try_or_error!(
+                    tasks.lock(),
+                    "tasks mutex lock failure",
                     Continue(false)
                 );
 
-                let devices = try_or_error!(
-                    devices.lock(),
-                    state.view,
-                    back,
-                    next,
-                    stack,
-                    error,
-                    "devices mutex lock failure",
-                    Continue(false)
-                );
-
-                let handle_iter = task_handles.deref_mut().drain(..);
-                let mut device_iter = devices.deref().iter();
-                for handle in handle_iter {
-                    if let Some(&(ref device, _)) = device_iter.next() {
-                        let result = try_or_error!(
-                            handle.join(),
-                            state.view,
-                            back,
-                            next,
-                            stack,
-                            error,
-                            "thread handle join failure",
-                            Continue(false)
-                        );
-
-                        if let Err(why) = result {
-                            errored.push((device.clone(), why));
-                        }
-                    }
+                ntasks = tasks.len();
+                if ntasks == 0 {
+                    return Continue(true);
                 }
 
-                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",
-                        ntasks - errored.len(),
-                        ntasks
-                    ));
-                    list.set_visible(true);
-                    for (device, why) in errored {
-                        let container = Box::new(Orientation::Horizontal, 0);
-                        let device = Label::new(device.as_str());
-                        let why = Label::new(format!("{}", why).as_str());
-                        container.pack_start(&device, false, false, 0);
-                        container.pack_start(&why, true, true, 0);
-                        list.insert(&container, -1);
+                for (task, &(ref pbar, ref label)) in tasks.deref().iter().zip(bars.borrow().iter()) {
+                    let raw_value = task.progress.load(Ordering::SeqCst);
+                    let task_is_finished = task.finished.load(Ordering::SeqCst) == 1;
+                    let value = if task_is_finished {
+                        1.0f64
+                    } else {
+                        all_tasks_finished = false;
+                        raw_value as f64 / image_length as f64
+                    };
+
+                    pbar.set_fraction(value);
+
+                    if task_is_finished {
+                        label.set_label("Complete");
+                    } else if let Ok(mut prev_values) = task.previous.lock() {
+                        prev_values[1] = prev_values[2];
+                        prev_values[2] = prev_values[3];
+                        prev_values[3] = prev_values[4];
+                        prev_values[4] = prev_values[5];
+                        prev_values[5] = prev_values[6];
+                        prev_values[6] = raw_value - prev_values[0];
+                        prev_values[0] = raw_value;
+
+                        let sum: usize = prev_values.iter().skip(1).sum();
+                        let per_second = sum / 3;
+                        label.set_label(&if per_second > (1024 * 1024) {
+                            format!("{} MiB/s", per_second / (1024 * 1024))
+                        } else {
+                            format!("{} KiB/s", per_second / 1024)
+                        });
                     }
                 }
+            }
 
-                Continue(false)
-            } else {
-                Continue(true)
+            if all_tasks_finished && widgets.switch_to_summary(&state, ntasks).is_err() {
+                return Continue(false);
             }
+
+            state.reset_after_reacquiring_image();
+
+            Continue(true)
         });
     }
 }
diff --git a/gtk/src/app/state/try.rs b/gtk/src/app/state/try.rs
index 10dd0fe..b55efc0 100644
--- a/gtk/src/app/state/try.rs
+++ b/gtk/src/app/state/try.rs
@@ -5,8 +5,8 @@ macro_rules! try_or_error {
         $view:expr,
         $back:expr,
         $next:expr,
-        $stack:ident,
-        $error:ident,
+        $stack:expr,
+        $error:expr,
         $msg:expr,
         $val:expr
     ) => {
diff --git a/gtk/src/flash.rs b/gtk/src/flash.rs
index c6942d7..db9da27 100644
--- a/gtk/src/flash.rs
+++ b/gtk/src/flash.rs
@@ -1,7 +1,6 @@
 use std::fs::File;
 use std::sync::atomic::{AtomicUsize, Ordering};
 use std::sync::Arc;
-
 use popsicle::{self, DiskError, PopsicleLog};
 
 pub struct FlashRequest {
@@ -9,6 +8,7 @@ pub struct FlashRequest {
     disk_path: String,
     image_len: u64,
     image_data: Arc<Vec<u8>>,
+    status:   Arc<AtomicUsize>,
     progress: Arc<AtomicUsize>,
     finished: Arc<AtomicUsize>,
 }
@@ -19,6 +19,7 @@ impl FlashRequest {
         disk_path: String,
         image_len: u64,
         image_data: Arc<Vec<u8>>,
+        status: Arc<AtomicUsize>,
         progress: Arc<AtomicUsize>,
         finished: Arc<AtomicUsize>,
     ) -> FlashRequest {
@@ -27,6 +28,7 @@ impl FlashRequest {
             disk_path,
             image_len,
             image_data,
+            status,
             progress,
             finished,
         }
@@ -38,13 +40,15 @@ impl FlashRequest {
         let progress = self.progress;
         let image_len = self.image_len;
         let image_data = self.image_data;
-
+        let status = self.status;
         let result = popsicle::write_to_disk(
             |log| match log {
                 PopsicleLog::Message(_) => (),
                 PopsicleLog::Finished => (),
                 PopsicleLog::Progress(value) => progress.store(value as usize, Ordering::SeqCst)
             },
+            // Write will exit early when this is true
+            || 4 == status.load(Ordering::SeqCst),
             disk,
             &disk_path,
             image_len,
@@ -53,7 +57,6 @@ impl FlashRequest {
         );
 
         self.finished.store(1, Ordering::SeqCst);
-
         result
     }
 }
diff --git a/gtk/src/main.rs b/gtk/src/main.rs
index fddd83f..b05779d 100644
--- a/gtk/src/main.rs
+++ b/gtk/src/main.rs
@@ -45,7 +45,7 @@ fn main() {
         devices_request_receiver,
         devices_response_sender,
         flash_request_receiver,
-        flash_response_sender
+        flash_response_sender,
     );
 
     // If running in pkexec or sudo, restore home directory for open dialog,
@@ -85,7 +85,7 @@ fn authenticated_threads(
     devices_request: Receiver<(Vec<String>, Vec<MountEntry>)>,
     devices_response: Sender<Result<Vec<(String, File)>, DiskError>>,
     flash_request: Receiver<FlashRequest>,
-    flash_response: Sender<JoinHandle<Result<(), DiskError>>>
+    flash_response: Sender<JoinHandle<Result<(), DiskError>>>,
 ) {
     thread::spawn(move || {
         loop {
diff --git a/screenshots/device-monitoring.gif b/screenshots/device-monitoring.gif
new file mode 100644
index 0000000..f3f07fb
Binary files /dev/null and b/screenshots/device-monitoring.gif differ
diff --git a/screenshots/screenshot-01.png b/screenshots/screenshot-01.png
new file mode 100644
index 0000000..d7aa285
Binary files /dev/null and b/screenshots/screenshot-01.png differ
diff --git a/screenshots/screenshot-02.png b/screenshots/screenshot-02.png
new file mode 100644
index 0000000..845aac6
Binary files /dev/null and b/screenshots/screenshot-02.png differ
diff --git a/screenshots/screenshot-03.png b/screenshots/screenshot-03.png
new file mode 100644
index 0000000..6d0c489
Binary files /dev/null and b/screenshots/screenshot-03.png differ
diff --git a/screenshots/screenshot-04.png b/screenshots/screenshot-04.png
new file mode 100644
index 0000000..b330516
Binary files /dev/null and b/screenshots/screenshot-04.png differ
diff --git a/screenshots/screenshot-05.png b/screenshots/screenshot-05.png
new file mode 100644
index 0000000..9b2c3e9
Binary files /dev/null and b/screenshots/screenshot-05.png differ
diff --git a/screenshots/screenshot-1.png b/screenshots/screenshot-1.png
deleted file mode 100644
index 66a6136..0000000
Binary files a/screenshots/screenshot-1.png and /dev/null differ
diff --git a/screenshots/screenshot-2.png b/screenshots/screenshot-2.png
deleted file mode 100644
index 01e83bb..0000000
Binary files a/screenshots/screenshot-2.png and /dev/null differ
diff --git a/screenshots/screenshot-3.png b/screenshots/screenshot-3.png
deleted file mode 100644
index 37a9892..0000000
Binary files a/screenshots/screenshot-3.png and /dev/null differ
diff --git a/screenshots/screenshot-4.png b/screenshots/screenshot-4.png
deleted file mode 100644
index a2115d6..0000000
Binary files a/screenshots/screenshot-4.png and /dev/null differ
diff --git a/src/lib.rs b/src/lib.rs
index 5aec31e..6cf0762 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -106,6 +106,8 @@ impl Image {
 pub enum DiskError {
     #[fail(display = "unable to open directory at '{}': {}", dir, why)]
     Directory { dir: &'static str, why: io::Error },
+    #[fail(display = "writing to the device was killed")]
+    Killed,
     #[fail(display = "unable to read directory entry at '{:?}': invalid UTF-8", dir)]
     UTF8 { dir: PathBuf },
     #[fail(display = "unable to find disk '{}': {}", disk, why)]
@@ -255,8 +257,9 @@ pub enum PopsicleLog<'a> {
 }
 
 /// Writes an image to the specified disk.
-pub fn write_to_disk<F: FnMut(PopsicleLog)>(
+pub fn write_to_disk<F: FnMut(PopsicleLog), K: FnMut() -> bool>(
     mut callback: F,
+    mut kill: K,
     mut disk: File,
     disk_path: &str,
     image_size: u64,
@@ -265,6 +268,10 @@ pub fn write_to_disk<F: FnMut(PopsicleLog)>(
 ) -> Result<(), DiskError> {
     let mut total = 0;
     while total < image_data.len() {
+        if kill() {
+            return Err(DiskError::Killed);
+        }
+
         let end = cmp::min(image_size as usize, total + BUFFER_SIZE);
         let count = disk.write(&image_data[total..end]).map_err(|why| {
             callback(PopsicleLog::Message(&format!("! {}: ", disk_path)));
@@ -326,6 +333,9 @@ pub fn write_to_disk<F: FnMut(PopsicleLog)>(
 
         let mut buf = vec![0; BUFFER_SIZE];
         while total < image_data.len() {
+            if kill() {
+                return Err(DiskError::Killed);
+            }
             let end = cmp::min(image_size as usize, total + BUFFER_SIZE);
             let count = match disk.read(&mut buf[..end - total]) {
                 Ok(count) => count,


Test patch: