commit 0b45b84e198adeef6a5ba4b9920cd25b6037960c
Author: dsorber <david.sorber@gmail.com>
Date:   Sun Sep 17 10:39:07 2017 -0400

    pwmgr v0.4.0 -- Added save warning before quit dialog.  Added improved state tracking. Finally, added progress pop on close to give user feedback while ramdisk cleanup is happening.

diff --git a/software/pwmgr/pwmgr.py b/software/pwmgr/pwmgr.py
index 71964d7..871ffc6 100755
--- a/software/pwmgr/pwmgr.py
+++ b/software/pwmgr/pwmgr.py
@@ -4,10 +4,12 @@ import os
 import shutil
 import subprocess
 import sys
+import time
+import threading
 
 import gi
 gi.require_version('Gtk', '3.0')
-from gi.repository import Gtk, Pango, Gdk
+from gi.repository import Gtk, Pango, Gdk, GLib
 
 MOUNT_PATH = '/mnt/pw_ramdisk'
 PWD_FILE_PATH = '/home/dsorber/Desktop/network'
@@ -17,7 +19,7 @@ SCRATCH_FILE_PATH = '/mnt/pw_ramdisk/chicken_soup.txt'
 ENC_TMP_FILE_PATH = '/home/dsorber/Documents/chicken_soup.enc.tmp'
 KEY_FILE_PATH = '/mnt/pw_ramdisk/.f05d11ef-000c-45c2-ae14-56b56c23e1c5'
 
-VERSION = '0.0.3'
+VERSION = '0.0.4'
 
 class SearchDialog(Gtk.Dialog):
 
@@ -44,7 +46,7 @@ class SearchDialog(Gtk.Dialog):
         self.show_all()
         
         
-class WarningDialog(Gtk.Dialog):
+class WarningDialog(Gtk.MessageDialog):
     
     def __init__(self, parent, title, message):
         Gtk.MessageDialog.__init__(self, parent,
@@ -54,6 +56,12 @@ class WarningDialog(Gtk.Dialog):
                                    message)
 
         self.set_title(title)
+        
+        # Make hitting ENTER trigger the OK button
+        ok_button = self.get_widget_for_response(response_id=Gtk.ResponseType.OK)
+        ok_button.set_can_default(True)
+        ok_button.grab_default()
+        
         self.show_all()
         
 def get_user_pw(parent, message, title=''):
@@ -73,6 +81,7 @@ def get_user_pw(parent, message, title=''):
     dialog.set_title(title)
 
     box = dialog.get_content_area()
+    box.set_border_width(10)
     pw_entry = Gtk.Entry()
     pw_entry.set_visibility(False)
     pw_entry.set_invisible_char('*')
@@ -95,7 +104,35 @@ def get_user_pw(parent, message, title=''):
     if (response == Gtk.ResponseType.OK) and (text != ''):
         return text
     else:
-        return None
+        return None    
+    
+class ProgressBarWindow(Gtk.Window):
+
+    def __init__(self, parent, title):
+        Gtk.Window.__init__(self, title=title)
+        self.set_border_width(10)
+
+        vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=3)
+        self.add(vbox)
+
+        self.progressbar = Gtk.ProgressBar()
+        vbox.pack_start(self.progressbar, True, True, 0)
+
+        self.progressbar.set_text('Zee text')
+        self.progressbar.set_show_text(True)
+        
+        self.set_modal(True)
+        #~ self.set_decorated(False)
+        self.set_deletable(False)
+        self.set_transient_for(parent)
+
+
+    def update_text(self, text):
+        self.progressbar.set_text(text)
+
+    def update_complete(self, fraction):
+        self.progressbar.set_fraction(fraction)
+
 
 class TextViewWindow(Gtk.Window):
 
@@ -114,9 +151,18 @@ class TextViewWindow(Gtk.Window):
         
         
         self.mounted = False
+        self.pass_data_loaded = False
         self.pwd_file_fd = None
-        
-        
+        self.pbar_win = None
+        self.should_quit = False
+    
+    def async_quit(self):        
+        if self.should_quit:
+            if self.pbar_win is not None:
+                self.pbar_win.destroy()
+            Gtk.main_quit()
+            print('REAL QUIT')
+        return True
 
     def _key_press_event(self, widget, event):
         keyval = event.keyval
@@ -207,9 +253,10 @@ class TextViewWindow(Gtk.Window):
         """
 
         # Get password
-        passwd = get_user_pw(self, 'Please enter your password', 'Password')
-        if passwd is None:
-            return
+        if not self.pass_data_loaded:
+            passwd = get_user_pw(self, 'Please enter your password', 'Password')
+            if passwd is None:
+                return
         
         if not self.mounted:
             os.makedirs(MOUNT_PATH, 0o600, exist_ok=True)
@@ -235,13 +282,16 @@ class TextViewWindow(Gtk.Window):
                 self.textbuffer.set_text(pwd_file_fd.read())
             self.textview.set_editable(True)
             self.textview.set_cursor_visible(True)
+            
+            # Mark state that pasword data is loaded
+            self.pass_data_loaded = True
         
     def save_pwd_file(self, widget):
         """
         Save handler
         """
         
-        if self.mounted:
+        if self.mounted and self.pass_data_loaded:
             
             # Write the password store datat to the sratch file
             with open(SCRATCH_FILE_PATH, 'w') as pwd_file_fd:
@@ -267,55 +317,87 @@ class TextViewWindow(Gtk.Window):
             self.textbuffer.set_text('')
             self.textview.set_editable(False)
             self.textview.set_cursor_visible(False)
+            
+            # Mark password data as not loaded
+            self.pass_data_loaded = False
     
     def _remove_mount_files(self):
         # Remove files in the mount path
         for file_ in glob.glob('{:s}/*'.format(MOUNT_PATH)):
             os.remove(file_)
     
-    def close_pwd_file(self, widget, other):
+    def do_mount_cleanup(self):
         """
-        Close handler
+        Clean up ramdisk by overwriting, then unmount
         """
+        GLib.idle_add(self.pbar_win.update_text, 'Starting ramdisk cleanup')
+        
         
-        #~ dialog = WarningDialog(self, 'Close?', 'Have you saved?')
-        #~ response = dialog.run()
-        #~ dialog.destroy()
-
-        # Clear the buffer and disable the textview
-        self.textbuffer.set_text('')
-        self.textview.set_editable(False)
-        self.textview.set_cursor_visible(False)
-
         rand_write_cmd = 'dd if=/dev/urandom of={:s} bs=16M count=1 status=none'
         rand_write_cmd = rand_write_cmd.format(os.path.join(MOUNT_PATH, 'data'))
         
         zero_write_cmd = 'dd if=/dev/zero of={:s} bs=16M count=1 status=none'
         zero_write_cmd = zero_write_cmd.format(os.path.join(MOUNT_PATH, 'data'))
         
-        if self.mounted:
+        self._remove_mount_files()
+            
+        # Overwrite the ramdisk a few times
+        for ctr in range(10):
+            print(ctr)
+            message = 'Overwrite {:d} of 10'.format(ctr +  1)
+            GLib.idle_add(self.pbar_win.update_text, message)
+            
+            subprocess.call(rand_write_cmd, shell=True, timeout=5)
+            self._remove_mount_files()
+            subprocess.call(zero_write_cmd, shell=True, timeout=5)
             self._remove_mount_files()
             
-            # Overwrite a few times
-            for ctr in range(10):
-                print(ctr)
-                subprocess.call(rand_write_cmd, shell=True, timeout=5)
-                self._remove_mount_files()
-                subprocess.call(zero_write_cmd, shell=True, timeout=5)
-                self._remove_mount_files()
+            GLib.idle_add(self.pbar_win.update_complete, (ctr + 1) / 10)
+        
+        GLib.idle_add(self.pbar_win.update_text, 'Unmounting ramdisk')
+        umount_cmd = 'umount {:s}'.format(MOUNT_PATH)
+        subprocess.call(umount_cmd, shell=True, timeout=5)
+        GLib.idle_add(self.pbar_win.update_text, 'Complete')
+        
+        self.should_quit = True
+        print('SET should quit')
+    
+    def close_pwd_file(self, widget, other):
+        """
+        Close handler
+        """
+        
+        # If data is loaded ask to save before closing
+        if self.pass_data_loaded:
+            dialog = WarningDialog(self, 'Save changes?', 
+                                   'Save changes to password data?')
+            response = dialog.run()
+            dialog.destroy()
+        
+            if response == Gtk.ResponseType.OK:
+                self.save_pwd_file(widget)
+        
+        # Clear the buffer and disable the textview
+        self.textbuffer.set_text('')
+        self.textview.set_editable(False)
+        self.textview.set_cursor_visible(False)
+        
+        if self.mounted:
+            # Start async polling of the quit flag
+            GLib.idle_add(self.async_quit)
+        
+            # Create the progress window
+            self.pbar_win = ProgressBarWindow(self, 'Closing...')
+            self.pbar_win.show_all()
             
-            umount_cmd = 'umount {:s}'.format(MOUNT_PATH)
-            subprocess.call(umount_cmd, shell=True, timeout=5)
+            # Start the work thread that does the cleanup
+            thread = threading.Thread(target=self.do_mount_cleanup)
+            thread.start()
+        else:
+            Gtk.main_quit()
         
-        Gtk.main_quit()
         print('CLOSE')
 
-    def on_button_clicked(self, widget, tag):
-        bounds = self.textbuffer.get_selection_bounds()
-        if len(bounds) != 0:
-            start, end = bounds
-            self.textbuffer.apply_tag(tag, start, end)
-
     def on_clear_clicked(self, widget):
         start = self.textbuffer.get_start_iter()
         end = self.textbuffer.get_end_iter()
