Modified og_gui_manager.py to make it accessible on devices not on the efabless platform. Changed the create project script to make the proper config directories so that the editors can be used. Modified profile.py to make the settings properly reflect the user preferences.
diff --git a/common/og_gui_manager.py b/common/og_gui_manager.py
new file mode 100755
index 0000000..921b3f1
--- /dev/null
+++ b/common/og_gui_manager.py
@@ -0,0 +1,4138 @@
+#!/ef/efabless/opengalaxy/venv/bin/python3 -B
+#
+#--------------------------------------------------------
+# Open Galaxy Project Manager GUI.
+#
+# This is a Python tkinter script that handles local
+# project management.  It is meant as a replacement for
+# appsel_zenity.sh
+#
+#--------------------------------------------------------
+# Written by Tim Edwards
+# efabless, inc.
+# September 9, 2016
+# Modifications 2017, 2018
+# Version 1.0
+#--------------------------------------------------------
+
+import sys
+# Require python 3.5.x (and not python 3.6.x). Without this trap here, in several
+# instances of VMs where /usr/bin/python3 symlinked to 3.6.x by mistake, it manifests
+# as (misleading) errors like: ImportError: No module named 'yaml'
+#
+# '%x' % sys.hexversion  ->  '30502f0'
+
+import tkinter
+from tkinter import ttk, StringVar, Listbox, END
+from tkinter import filedialog
+
+# globals
+theProg = sys.argv[0]
+root = tkinter.Tk()      # WARNING: must be exactly one instance of Tk; don't call again elsewhere
+
+# 4 configurations based on booleans: splash,defer
+# n,n:  no splash, show only form when completed: LEGACY MODE, user confused by visual lag.
+# n,y:  no splash but defer projLoad: show an empty form ASAP
+# y,n:  yes splash, and wait for projLoad before showing completed form
+# y,y:  yes splash, but also defer projLoad: show empty form ASAP
+
+# deferLoad = False        # LEGACY: no splash, and wait for completed form
+# doSplash = False
+
+deferLoad = True         # True: display GUI before (slow) loading of projects, so no splash:
+doSplash = not deferLoad # splash IFF GUI-construction includes slow loading of projects
+
+# deferLoad = False        # load projects before showing form, so need splash:
+# doSplash = not deferLoad # splash IFF GUI-construction includes slow loading of projects
+
+# deferLoad = True         # here keep splash also, despite also deferred-loading
+# doSplash = True
+
+#------------------------------------------------------
+# Splash screen: display ASAP: BEFORE bulk of imports.
+#------------------------------------------------------
+
+class SplashScreen(tkinter.Toplevel):
+    """Open Galaxy Project Management Splash Screen"""
+
+    def __init__(self, parent, *args, **kwargs):
+        super().__init__(parent, *args, **kwargs)
+        parent.withdraw()
+        #EFABLESS PLATFORM
+        #image = tkinter.PhotoImage(file="/ef/efabless/opengalaxy/og_splashscreen50.gif")
+        label = ttk.Label(self, image=image)
+        label.pack()
+
+        # required to make window show before the program gets to the mainloop
+        self.update_idletasks()
+
+import faulthandler
+import signal
+
+# SplashScreen here. fyi: there's a 2nd/later __main__ section for main app
+splash = None     # a global
+if __name__ == '__main__':
+    faulthandler.register(signal.SIGUSR2)
+    if doSplash:
+        splash = SplashScreen(root)
+
+import io
+import os
+import re
+import json
+import yaml
+import shutil
+import tarfile
+import datetime
+import subprocess
+import contextlib
+import tempfile
+import glob
+
+import tksimpledialog
+import tooltip
+from rename_project import rename_project_all
+#from fix_libdirs import fix_libdirs
+from consoletext import ConsoleText
+from helpwindow import HelpWindow
+from treeviewchoice import TreeViewChoice
+from symbolbuilder import SymbolBuilder
+from make_icon_from_soft import create_symbol
+from profile import Profile
+
+import og_config
+
+# Global name for design directory
+designdir = 'design'
+# Global name for import directory
+importdir = 'import'
+# Global name for cloudv directory
+cloudvdir = 'cloudv'
+# Global name for archived imports project sub-directory
+archiveimportdir = 'imported'
+# Global name for current design file
+#EFABLESS PLATFORM
+currdesign = '~/.open_pdks/currdesign'
+prefsfile = '~/.open_pdks/prefs.json'
+
+
+#---------------------------------------------------------------
+# Watch a directory for modified time change.  Repeat every two
+# seconds.  Call routine callback() if a change occurs
+#---------------------------------------------------------------
+
+class WatchClock(object):
+    def __init__(self, parent, path, callback, interval=2000, interval0=None):
+        self.parent = parent
+        self.callback = callback
+        self.path = path
+        self.interval = interval
+        if interval0 != None:
+            self.interval0 = interval0
+            self.restart(first=True)
+        else:
+            self.interval0 = interval
+            self.restart()
+
+    def query(self):
+        for entry in self.path:
+            statbuf = os.stat(entry)
+            if statbuf.st_mtime > self.reftime:
+                self.callback()
+                self.restart()
+                return
+        self.timer = self.parent.after(self.interval, self.query)
+
+    def stop(self):
+        self.parent.after_cancel(self.timer)
+
+    # if first: optionally use different (typically shorter) interval, AND DON'T
+    # pre-record watched-dir mtime-s (which forces the callback on first timer fire)
+    def restart(self, first=False):
+        self.reftime = 0
+        if not first:
+            for entry in self.path:
+                statbuf = os.stat(entry)
+                if statbuf.st_mtime > self.reftime:
+                    self.reftime = statbuf.st_mtime
+        self.timer = self.parent.after(self.interval0 if first and self.interval0 != None else self.interval, self.query)
+
+#------------------------------------------------------
+# Dialog for generating a new layout
+#------------------------------------------------------
+
+class NewLayoutDialog(tksimpledialog.Dialog):
+    def body(self, master, warning, seed=''):
+        if warning:
+            ttk.Label(master, text=warning).grid(row = 0, columnspan = 2, sticky = 'wns')
+
+        self.l1prefs = tkinter.IntVar(master)
+        self.l1prefs.set(1)
+        ttk.Checkbutton(master, text='Populate new layout from netlist',
+			variable = self.l1prefs).grid(row = 2, columnspan = 2, sticky = 'enws')
+
+        return self
+
+    def apply(self):
+        return self.l1prefs.get
+
+#------------------------------------------------------
+# Simple dialog for entering project names
+#------------------------------------------------------
+
+class ProjectNameDialog(tksimpledialog.Dialog):
+    def body(self, master, warning, seed=''):
+        if warning:
+            ttk.Label(master, text=warning).grid(row = 0, columnspan = 2, sticky = 'wns')
+        ttk.Label(master, text='Enter new project name:').grid(row = 1, column = 0, sticky = 'wns')
+        self.nentry = ttk.Entry(master)
+        self.nentry.grid(row = 1, column = 1, sticky = 'ewns')
+        self.nentry.insert(0, seed)
+        return self.nentry # initial focus
+
+    def apply(self):
+        return self.nentry.get()
+
+class PadFrameCellNameDialog(tksimpledialog.Dialog):
+    def body(self, master, warning, seed=''):
+        description='PadFrame'       # TODO: make this an extra optional parameter of a generic CellNameDialog?
+        if warning:
+            ttk.Label(master, text=warning).grid(row = 0, columnspan = 2, sticky = 'wns')
+        if description:
+            description = description + " "
+        else:
+            description = ""
+        ttk.Label(master, text=("Enter %scell name:" %(description))).grid(row = 1, column = 0, sticky = 'wns')
+        self.nentry = ttk.Entry(master)
+        self.nentry.grid(row = 1, column = 1, sticky = 'ewns')
+        self.nentry.insert(0, seed)
+        return self.nentry # initial focus
+
+    def apply(self):
+        return self.nentry.get()
+
+#------------------------------------------------------
+# Dialog for copying projects.  Includes checkbox
+# entries for preferences.
+#------------------------------------------------------
+
+class CopyProjectDialog(tksimpledialog.Dialog):
+    def body(self, master, warning, seed=''):
+        if warning:
+            ttk.Label(master, text=warning).grid(row = 0, columnspan = 2, sticky = 'wns')
+        ttk.Label(master, text="Enter new project name:").grid(row = 1, column = 0, sticky = 'wns')
+        self.nentry = ttk.Entry(master)
+        self.nentry.grid(row = 1, column = 1, sticky = 'ewns')
+        self.nentry.insert(0, seed)
+        self.elprefs = tkinter.IntVar(master)
+        self.elprefs.set(0)
+        ttk.Checkbutton(master, text='Copy electric preferences (not recommended)',
+			variable = self.elprefs).grid(row = 2, columnspan = 2, sticky = 'enws')
+        self.spprefs = tkinter.IntVar(master)
+        self.spprefs.set(0)
+        ttk.Checkbutton(master, text='Copy ngspice folder (not recommended)',
+			variable = self.spprefs).grid(row = 3, columnspan = 2, sticky = 'enws')
+        return self.nentry # initial focus
+
+    def apply(self):
+        # Return a list containing the entry text and the checkbox states.
+        elprefs = True if self.elprefs.get() == 1 else False
+        spprefs = True if self.spprefs.get() == 1 else False
+        return [self.nentry.get(), elprefs, spprefs]
+
+#-------------------------------------------------------
+# Not-Quite-So-Simple dialog for entering a new project.
+# Select a project name and a PDK from a drop-down list.
+#-------------------------------------------------------
+
+class NewProjectDialog(tksimpledialog.Dialog):
+    def body(self, master, warning, seed='', importnode=None, development=False):
+        if warning:
+            ttk.Label(master, text=warning).grid(row = 0, columnspan = 2, sticky = 'wns')
+        ttk.Label(master, text="Enter new project name:").grid(row = 1, column = 0)
+        self.nentry = ttk.Entry(master)
+        self.nentry.grid(row = 1, column = 1, sticky = 'ewns')
+        self.nentry.insert(0, seed or '')      # may be None
+        self.pvar = tkinter.StringVar(master)
+        if not importnode:
+            # Add PDKs as found by searching /ef/tech for 'libs.tech' directories
+            ttk.Label(master, text="Select foundry/node:").grid(row = 2, column = 0)
+        else:
+            ttk.Label(master, text="Foundry/node:").grid(row = 2, column = 0)
+        self.infolabel = ttk.Label(master, text="", style = 'brown.TLabel', wraplength=250)
+        self.infolabel.grid(row = 3, column = 0, columnspan = 2, sticky = 'news')
+        self.pdkmap = {}
+        self.pdkdesc = {}
+        self.pdkstat = {}
+        pdk_def = None
+
+        node_def = importnode
+        if not node_def:
+            node_def = "EFXH035B"
+
+        # use glob instead of os.walk. Don't need to recurse large PDK hier.
+        # TODO: stop hardwired default EFXH035B: get from an overall flow /ef/tech/.ef-config/plist.json
+        # (or get it from the currently selected project)
+        #EFABLESS PLATFORM
+        #TODO: Replace with PREFIX
+        for pdkdir_lr in glob.glob('/usr/share/pdk/*/libs.tech/'):
+            pdkdir = os.path.split( os.path.split( pdkdir_lr )[0])[0]    # discard final .../libs.tech/
+            (foundry, node, desc, status) = OpenGalaxyManager.pdkdir2fnd( pdkdir )
+            if not foundry or not node:
+                continue
+            key = foundry + '/' + node
+            self.pdkmap[key] = pdkdir
+            self.pdkdesc[key] = desc
+            self.pdkstat[key] = status
+            if node == node_def and not pdk_def:
+                pdk_def = key
+            
+        # Quick hack:  sorting puts EFXH035A before EFXH035LEGACY.  However, some
+        # ranking is needed.
+        pdklist = sorted( self.pdkmap.keys())
+        if not pdklist:
+            raise ValueError( "assertion failed, no available PDKs found")
+        pdk_def = (pdk_def or pdklist[0])
+
+        self.pvar.set(pdk_def)
+
+        # Restrict list to single entry if importnode was non-NULL and
+        # is in the PDK list (OptionMenu is replaced by a simple label)
+        # Otherwise, restrict the list to entries having an "status"
+        # entry equal to "active".  This allows some legacy PDKs to be
+	# disabled for creating new projects (but available for projects
+        # that already have them).
+        
+        if importnode:
+            self.pdkselect = ttk.Label(master, text = pdk_def, style='blue.TLabel')
+        else:
+            pdkactive = list(item for item in pdklist if self.pdkstat[item] == 'active')
+            if development:
+                pdkactive.extend(list(item for item in pdklist if self.pdkstat[item] == 'development'))
+
+            self.pdkselect = ttk.OptionMenu(master, self.pvar, pdk_def, *pdkactive,
+    			style='blue.TMenubutton', command=self.show_info)
+        self.pdkselect.grid(row = 2, column = 1)
+        self.show_info(0)
+
+        return self.nentry # initial focus
+
+    def show_info(self, args):
+        key = str(self.pvar.get())
+        desc = self.pdkdesc[key]
+        if desc == '':
+            self.infolabel.config(text='(no description available)')
+        else:
+            self.infolabel.config(text=desc)
+
+    def apply(self):
+        return self.nentry.get(), self.pdkmap[ str(self.pvar.get()) ]  # Note converts StringVar to string
+
+#----------------------------------------------------------------
+# Not-Quite-So-Simple dialog for selecting an existing project.
+# Select a project name from a drop-down list.  This could be
+# replaced by simply using the selected (current) project.
+#----------------------------------------------------------------
+
+class ExistingProjectDialog(tksimpledialog.Dialog):
+    def body(self, master, plist, seed, warning='Enter name of existing project to import into:'):
+        ttk.Label(master, text=warning).grid(row = 0, columnspan = 2, sticky = 'wns')
+
+        # Alphebetize list
+        plist.sort()
+        # Add projects
+        self.pvar = tkinter.StringVar(master)
+        self.pvar.set(plist[0])
+
+        ttk.Label(master, text='Select project:').grid(row = 1, column = 0)
+
+        self.projectselect = ttk.OptionMenu(master, self.pvar, plist[0], *plist, style='blue.TMenubutton')
+        self.projectselect.grid(row = 1, column = 1, sticky = 'ewns')
+        # pack version (below) hangs. Don't know why, changed to grid (like ProjectNameDialog)
+        # self.projectselect.pack(side = 'top', fill = 'both', expand = 'true')
+        return self.projectselect # initial focus
+
+    def apply(self):
+        return self.pvar.get()  # Note converts StringVar to string
+
+#----------------------------------------------------------------
+# Not-Quite-So-Simple dialog for selecting an existing ElecLib of existing project.
+# Select an elecLib name from a drop-down list.
+#----------------------------------------------------------------
+
+class ExistingElecLibDialog(tksimpledialog.Dialog):
+    def body(self, master, plist, seed):
+        warning = "Enter name of existing Electric library to import into:"
+        ttk.Label(master, text=warning).grid(row = 0, columnspan = 2, sticky = 'wns')
+
+        # Alphebetize list
+        plist.sort()
+        # Add electric libraries
+        self.pvar = tkinter.StringVar(master)
+        self.pvar.set(plist[0])
+
+        ttk.Label(master, text="Select library:").grid(row = 1, column = 0)
+
+        self.libselect = ttk.OptionMenu(master, self.pvar, plist[0], *plist, style='blue.TMenubutton')
+        self.libselect.grid(row = 1, column = 1)
+        return self.libselect # initial focus
+
+    def apply(self):
+        return self.pvar.get()  # Note converts StringVar to string
+
+#----------------------------------------------------------------
+# Dialog for layout, in case of multiple layout names, none of
+# which matches the project name (ip-name).  Method: Select a
+# layout name from a drop-down list.  If there is no project.json
+# file, add a checkbox for creating one and seeding the ip-name
+# with the name of the selected layout.  Include entry for
+# new layout, and for new layouts add a checkbox to import the
+# layout from schematic or verilog, if a valid candidate exists.
+#----------------------------------------------------------------
+
+class EditLayoutDialog(tksimpledialog.Dialog):
+    def body(self, master, plist, seed='', ppath='', pname='', warning='', hasnet=False):
+        ttk.Label(master, text=warning).grid(row = 0, columnspan = 2, sticky = 'wns')
+        self.ppath = ppath
+        self.pname = pname
+
+        # Checkbox variable
+        self.confirm = tkinter.IntVar(master)
+        self.confirm.set(0)
+
+        # To-Do:  Add checkbox for netlist import
+
+        # Alphebetize list
+        plist.sort()
+        # Add additional item for new layout
+        plist.append('(New layout)')
+
+        # Add layouts to list
+        self.pvar = tkinter.StringVar(master)
+        self.pvar.set(plist[0])
+
+        ttk.Label(master, text='Selected layout to edit:').grid(row = 1, column = 0)
+
+        if pname in plist:
+            pseed = plist.index(pname)
+        else:
+            pseed = 0
+
+        self.layoutselect = ttk.OptionMenu(master, self.pvar, plist[pseed], *plist,
+				style='blue.TMenubutton', command=self.handle_choice)
+        self.layoutselect.grid(row = 1, column = 1, sticky = 'ewns')
+
+        # Create an entry form and checkbox for entering a new layout name, but
+        # keep them unpacked unless the "(New layout)" selection is chosen.
+
+        self.layoutbox = ttk.Frame(master)
+        self.layoutlabel = ttk.Label(self.layoutbox, text='New layout name:')
+        self.layoutlabel.grid(row = 0, column = 0, sticky = 'ewns')
+        self.layoutentry = ttk.Entry(self.layoutbox)
+        self.layoutentry.grid(row = 0, column = 1, sticky = 'ewns')
+        self.layoutentry.insert(0, pname)
+
+        # Only allow 'makeproject' checkbox if there is no project.json file
+        jname = ppath + '/project.json'
+        if not os.path.exists(jname):
+            dname = os.path.split(ppath)[1]
+            jname = ppath + '/' + dname + '.json'
+            if not os.path.exists(jname):
+                self.makeproject = ttk.Checkbutton(self.layoutbox,
+			text='Make default project name',
+			variable = self.confirm)
+                self.makeproject.grid(row = 2, column = 0, columnspan = 2, sticky = 'ewns')
+        return self.layoutselect # initial focus
+
+    def handle_choice(self, event):
+        if self.pvar.get() == '(New layout)':
+            # Add entry and checkbox for creating ad-hoc project.json file
+            self.layoutbox.grid(row = 1, column = 0, columnspan = 2, sticky = 'ewns')
+        else:
+            # Remove entry and checkbox
+            self.layoutbox.grid_forget()
+        return
+
+    def apply(self):
+        if self.pvar.get() == '(New layout)':
+            if self.confirm.get() == 1:
+                pname = self.pname
+                master.create_ad_hoc_json(self.layoutentry.get(), pname)
+            return self.layoutentry.get()
+        else:
+            return self.pvar.get()  # Note converts StringVar to string
+
+#----------------------------------------------------------------
+# Dialog for padframe: select existing ElecLib of existing project, type in a cellName.
+#   Select an elecLib name from a drop-down list.
+#   Text field for entry of a cellName.
+#----------------------------------------------------------------
+
+class ExistingElecLibCellDialog(tksimpledialog.Dialog):
+    def body(self, master, descPre, seed='', descPost='', plist=None, seedLibNm=None, seedCellNm=''):
+        warning = 'Pick existing Electric library; enter cell name'
+        warning = (descPre or '') + ((descPre and ': ') or '') + warning + ((descPost and ' ') or '') + (descPost or '')
+        ttk.Label(master, text=warning).grid(row = 0, columnspan = 2, sticky = 'wns')
+
+        # Alphebetize list
+        plist.sort()
+        # Add electric libraries
+        self.pvar = tkinter.StringVar(master)
+        pNdx = 0
+        if seedLibNm and seedLibNm in plist:
+            pNdx = plist.index(seedLibNm)
+        self.pvar.set(plist[pNdx])
+
+        ttk.Label(master, text='Electric library:').grid(row = 1, column = 0, sticky = 'ens')
+        self.libselect = ttk.OptionMenu(master, self.pvar, plist[pNdx], *plist, style='blue.TMenubutton')
+        self.libselect.grid(row = 1, column = 1, sticky = 'wns')
+
+        ttk.Label(master, text=('cell name:')).grid(row = 2, column = 0, sticky = 'ens')
+        self.nentry = ttk.Entry(master)
+        self.nentry.grid(row = 2, column = 1, sticky = 'ewns')
+        self.nentry.insert(0, seedCellNm)
+
+        return self.libselect # initial focus
+
+    def apply(self):
+        # return list of 2 strings: selected ElecLibName, typed-in cellName.
+        return [self.pvar.get(), self.nentry.get()]  # Note converts StringVar to string
+
+#------------------------------------------------------
+# Simple dialog for confirming anything.
+#------------------------------------------------------
+
+class ConfirmDialog(tksimpledialog.Dialog):
+    def body(self, master, warning, seed):
+        if warning:
+            ttk.Label(master, text=warning).grid(row = 0, columnspan = 2, sticky = 'wns')
+        return self
+
+    def apply(self):
+        return 'okay'
+
+#------------------------------------------------------
+# More proactive dialog for confirming an invasive
+# procedure like "delete project".  Requires user to
+# click a checkbox to ensure this is not a mistake.
+# confirmPrompt can be overridden, default='I am sure I want to do this.'
+#------------------------------------------------------
+
+class ProtectedConfirmDialog(tksimpledialog.Dialog):
+    def body(self, master, warning, seed='', confirmPrompt=None):
+        if warning:
+            ttk.Label(master, text=warning).grid(row = 0, columnspan = 2, sticky = 'wns')
+        self.confirm = tkinter.IntVar(master)
+        self.confirm.set(0)
+        if not confirmPrompt:
+            confirmPrompt='I am sure I want to do this.'
+        ttk.Checkbutton(master, text=confirmPrompt,
+			variable = self.confirm).grid(row = 1, columnspan = 2, sticky = 'enws')
+        return self
+
+    def apply(self):
+        return 'okay' if self.confirm.get() == 1 else ''
+
+#------------------------------------------------------
+# Simple dialog to say "blah is not implemented yet."
+#------------------------------------------------------
+
+class NotImplementedDialog(tksimpledialog.Dialog):
+    def body(self, master, warning, seed):
+        if not warning:
+            warning = "Sorry, that feature is not implemented yet"
+        if warning:
+            warning = "Sorry, " + warning + ", is not implemented yet"
+            ttk.Label(master, text=warning).grid(row = 0, columnspan = 2, sticky = 'wns')
+        return self
+
+    def apply(self):
+        return 'okay'
+
+#------------------------------------------------------
+# (This is actually a generic confirm dialogue, no install/overwrite intelligence)
+# But so far dedicated to confirming the installation of one or more files,
+# with notification of which (if any) will overwrite existing files.
+#
+# The warning parameter is fully constructed by caller, as multiple lines as either:
+#   For the import of module 'blah',
+#   CONFIRM installation of (*: OVERWRITE existing):
+#    * path1
+#      path2
+#      ....
+# or:
+#   For the import of module 'blah',
+#   CONFIRM installation of:
+#      path1
+#      path2
+#      ....
+# TODO: bastardizes warning parameter as multiple lines. Implement some other way?
+#------------------------------------------------------
+
+class ConfirmInstallDialog(tksimpledialog.Dialog):
+    def body(self, master, warning, seed):
+        if warning:
+            ttk.Label(master, text=warning).grid(row = 0, columnspan = 2, sticky = 'wns')
+        return self
+
+    def apply(self):
+        return 'okay'
+
+#------------------------------------------------------
+# Open Galaxy Manager class
+#------------------------------------------------------
+
+class OpenGalaxyManager(ttk.Frame):
+    """Open Galaxy Project Management GUI."""
+
+    def __init__(self, parent, *args, **kwargs):
+        super().__init__(parent, *args, **kwargs)
+        self.root = parent
+        parent.withdraw()
+        # self.update()
+        self.update_idletasks()     # erase small initial frame asap
+        self.init_gui()
+        parent.protocol("WM_DELETE_WINDOW", self.on_quit)
+        if splash:
+            splash.destroy()
+        parent.deiconify()
+
+    def on_quit(self):
+        """Exits program."""
+        quit()
+
+    def init_gui(self):
+        """Builds GUI."""
+        global designdir
+        global importdir
+        global archiveimportdir
+        global currdesign
+        global theProg
+        global deferLoad
+
+        message = []
+        allPaneOpen = False
+        prjPaneMinh = 10
+        iplPaneMinh = 4
+        impPaneMinh = 4
+
+        # if deferLoad:         # temp. for testing... open all panes
+        #     allPaneOpen = True
+
+        # Read user preferences
+        self.prefs = {}
+        self.read_prefs()
+
+        # Get default font size from user preferences
+        fontsize = self.prefs['fontsize']
+
+        s = ttk.Style()
+        available_themes = s.theme_names()
+        # print("themes: " + str(available_themes))
+        s.theme_use(available_themes[0])
+
+        s.configure('gray.TFrame', background='gray40')
+        s.configure('blue_white.TFrame', bordercolor = 'blue', borderwidth = 3)
+        s.configure('italic.TLabel', font=('Helvetica', fontsize, 'italic'))
+        s.configure('title.TLabel', font=('Helvetica', fontsize, 'bold italic'),
+                        foreground = 'brown', anchor = 'center')
+        s.configure('title2.TLabel', font=('Helvetica', fontsize, 'bold italic'),
+                        foreground = 'blue')
+        s.configure('normal.TLabel', font=('Helvetica', fontsize))
+        s.configure('red.TLabel', font=('Helvetica', fontsize), foreground = 'red')
+        s.configure('brown.TLabel', font=('Helvetica', fontsize), foreground = 'brown3', background = 'gray95')
+        s.configure('green.TLabel', font=('Helvetica', fontsize), foreground = 'green3')
+        s.configure('blue.TLabel', font=('Helvetica', fontsize), foreground = 'blue')
+        s.configure('normal.TButton', font=('Helvetica', fontsize), border = 3, relief = 'raised')
+        s.configure('red.TButton', font=('Helvetica', fontsize), foreground = 'red', border = 3,
+                        relief = 'raised')
+        s.configure('green.TButton', font=('Helvetica', fontsize), foreground = 'green3', border = 3,
+                        relief = 'raised')
+        s.configure('blue.TMenubutton', font=('Helvetica', fontsize), foreground = 'blue', border = 3,
+			relief = 'raised')
+
+        # Create the help window
+        self.help = HelpWindow(self, fontsize=fontsize)
+        
+        with io.StringIO() as buf, contextlib.redirect_stdout(buf):
+            self.help.add_pages_from_file(og_config.apps_path + '/manager_help.txt')
+            message = buf.getvalue()
+        
+
+        # Set the help display to the first page
+        self.help.page(0)
+
+        # Create the profile settings window
+        self.profile = Profile(self, fontsize=fontsize)
+
+        # Variables used by option menus
+        self.seltype = tkinter.StringVar(self)
+        self.cur_project = tkinter.StringVar(self)
+        self.cur_import = "(nothing selected)"
+        self.project_name = ""
+
+        # Root window title
+        self.root.title('Open Galaxy Project Manager')
+        self.root.option_add('*tearOff', 'FALSE')
+        self.pack(side = 'top', fill = 'both', expand = 'true')
+
+        pane = tkinter.PanedWindow(self, orient = 'vertical', sashrelief='groove', sashwidth=6)
+        pane.pack(side = 'top', fill = 'both', expand = 'true')
+        self.toppane = ttk.Frame(pane)
+        self.botpane = ttk.Frame(pane)
+
+        # All interior windows size to toppane
+        self.toppane.columnconfigure(0, weight = 1)
+        # Projects window resizes preferably to others
+        self.toppane.rowconfigure(3, weight = 1)
+
+        # Get username, and from it determine the project directory.
+        # Save this path, because it gets used often.
+        username = self.prefs['username']
+        self.projectdir = os.path.expanduser('~/' + designdir)
+        self.cloudvdir = os.path.expanduser('~/' + cloudvdir)
+
+        # Check that the project directory exists, and create it if not
+        if not os.path.isdir(self.projectdir):
+            os.makedirs(self.projectdir)
+
+        # Label with the user
+        self.toppane.user_frame = ttk.Frame(self.toppane)
+        self.toppane.user_frame.grid(row = 0, sticky = 'news')
+
+        # Put logo image in corner.  Ignore if something goes wrong, as this
+        # is only decorative.  Note: ef_logo must be kept as a record in self,
+        # or else it gets garbage collected.
+        try:
+            #EFABLESS PLATFORM
+            self.ef_logo = tkinter.PhotoImage(file='/ef/efabless/opengalaxy/efabless_logo_small.gif')
+            self.toppane.user_frame.logo = ttk.Label(self.toppane.user_frame, image=self.ef_logo)
+            self.toppane.user_frame.logo.pack(side = 'left', padx = 5)
+        except:
+            pass
+
+        self.toppane.user_frame.title = ttk.Label(self.toppane.user_frame, text='User:', style='red.TLabel')
+        self.toppane.user_frame.user = ttk.Label(self.toppane.user_frame, text=username, style='blue.TLabel')
+
+        self.toppane.user_frame.title.pack(side = 'left', padx = 5)
+        self.toppane.user_frame.user.pack(side = 'left', padx = 5)
+
+        #---------------------------------------------
+        ttk.Separator(self.toppane, orient='horizontal').grid(row = 1, sticky = 'news')
+        #---------------------------------------------
+
+        # List of projects:
+        self.toppane.design_frame = ttk.Frame(self.toppane)
+        self.toppane.design_frame.grid(row = 2, sticky = 'news')
+
+        self.toppane.design_frame.design_header = ttk.Label(self.toppane.design_frame, text='Projects',
+			style='title.TLabel')
+        self.toppane.design_frame.design_header.pack(side = 'left', padx = 5)
+
+        self.toppane.design_frame.design_header2 = ttk.Label(self.toppane.design_frame,
+			text='(' + self.projectdir + '/)', style='normal.TLabel')
+        self.toppane.design_frame.design_header2.pack(side = 'left', padx = 5)
+
+        # Get current project from ~/.efmeta/currdesign and set the selection.
+        try:
+            with open(os.path.expanduser(currdesign), 'r') as f:
+                pnameCur = f.read().rstrip()
+        except:
+            pnameCur = None
+
+        # Create listbox of projects
+        projectlist = self.get_project_list() if not deferLoad else []
+        height = min(10, max(prjPaneMinh, 2 + len(projectlist)))
+        self.projectselect = TreeViewChoice(self.toppane, fontsize=fontsize, deferLoad=deferLoad, selectVal=pnameCur, natSort=True)
+        self.projectselect.populate("Available Projects:", projectlist,
+			[["Create", True, self.createproject],
+			 ["Copy", False, self.copyproject],
+			 ["Rename IP", False, self.renameproject],
+			 ["<CloudV", True, self.cloudvimport],
+			 ["Clean", False, self.cleanproject],
+			 ["Delete", False, self.deleteproject]],
+			height=height, columns=[0, 1])
+        self.projectselect.grid(row = 3, sticky = 'news')
+        self.projectselect.bindselect(self.setcurrent)
+
+        tooltip.ToolTip(self.projectselect.get_button(0), text="Create a new project")
+        tooltip.ToolTip(self.projectselect.get_button(1), text="Make a copy of an entire project")
+        tooltip.ToolTip(self.projectselect.get_button(2), text="Rename a project folder")
+        tooltip.ToolTip(self.projectselect.get_button(3), text="Import CloudV project as new project")
+        tooltip.ToolTip(self.projectselect.get_button(4), text="Clean simulation data from project")
+        tooltip.ToolTip(self.projectselect.get_button(5), text="Delete an entire project")
+
+        pdklist = self.get_pdk_list(projectlist)
+        self.projectselect.populate2("PDK", projectlist, pdklist)
+
+        if pnameCur:
+            try:
+                curitem = next(item for item in projectlist if pnameCur == os.path.split(item)[1])
+            except StopIteration:
+                pass
+            else:
+                if curitem:
+                    self.projectselect.setselect(pnameCur)
+
+        # Check that the import directory exists, and create it if not
+        if not os.path.isdir(self.projectdir + '/' + importdir):
+            os.makedirs(self.projectdir + '/' + importdir)
+
+        # Create a watchdog on the project and import directories
+        watchlist = [self.projectdir, self.projectdir + '/' + importdir]
+        if os.path.isdir(self.projectdir + '/upload'):
+            watchlist.append(self.projectdir + '/upload')
+        
+        # Check the creation time of the project manager app itself.  Because the project
+        # manager tends to be left running indefinitely, it is important to know when it
+        # has been updated.  This is checked once every hour since it is really expected
+        # only to happen occasionally.
+
+        thisapp = [theProg]
+        self.watchself = WatchClock(self, thisapp, self.update_alert, 3600000)
+
+        #---------------------------------------------
+
+        # Add second button bar for major project applications
+        self.toppane.apptitle = ttk.Label(self.toppane, text='Tools:', style='title2.TLabel')
+        self.toppane.apptitle.grid(row = 4, sticky = 'news')
+        self.toppane.appbar = ttk.Frame(self.toppane)
+        self.toppane.appbar.grid(row = 5, sticky = 'news')
+
+        # Define the application buttons and actions
+        self.toppane.appbar.schem_button = ttk.Button(self.toppane.appbar, text='Edit Schematic',
+		command=self.edit_schematic, style = 'normal.TButton')
+        self.toppane.appbar.schem_button.pack(side = 'left', padx = 5)
+        self.toppane.appbar.layout_button = ttk.Button(self.toppane.appbar, text='Edit Layout',
+		command=self.edit_layout, style = 'normal.TButton')
+        self.toppane.appbar.layout_button.pack(side = 'left', padx = 5)
+        self.toppane.appbar.lvs_button = ttk.Button(self.toppane.appbar, text='Run LVS',
+		command=self.run_lvs, style = 'normal.TButton')
+        self.toppane.appbar.lvs_button.pack(side = 'left', padx = 5)
+        self.toppane.appbar.char_button = ttk.Button(self.toppane.appbar, text='Characterize',
+		command=self.characterize, style = 'normal.TButton')
+        self.toppane.appbar.char_button.pack(side = 'left', padx = 5)
+        self.toppane.appbar.synth_button = ttk.Button(self.toppane.appbar, text='Synthesis Flow',
+		command=self.synthesize, style = 'normal.TButton')
+        self.toppane.appbar.synth_button.pack(side = 'left', padx = 5)
+
+        self.toppane.appbar.padframeCalc_button = ttk.Button(self.toppane.appbar, text='Pad Frame',
+        	command=self.padframe_calc, style = 'normal.TButton')
+        self.toppane.appbar.padframeCalc_button.pack(side = 'left', padx = 5)
+        '''
+        if self.prefs['schemeditor'] == 'xcircuit':
+            tooltip.ToolTip(self.toppane.appbar.schem_button, text="Start 'XCircuit' schematic editor")
+        elif self.prefs['schemeditor'] == 'xschem':
+            tooltip.ToolTip(self.toppane.appbar.schem_button, text="Start 'XSchem' schematic editor")
+        else:
+            tooltip.ToolTip(self.toppane.appbar.schem_button, text="Start 'Electric' schematic editor")
+
+        if self.prefs['layouteditor'] == 'klayout':
+            tooltip.ToolTip(self.toppane.appbar.layout_button, text="Start 'KLayout' layout editor")
+        else:
+            tooltip.ToolTip(self.toppane.appbar.layout_button, text="Start 'Magic' layout editor")
+        '''  
+        self.refreshToolTips()
+        
+        tooltip.ToolTip(self.toppane.appbar.lvs_button, text="Start LVS tool")
+        tooltip.ToolTip(self.toppane.appbar.char_button, text="Start Characterization tool")
+        tooltip.ToolTip(self.toppane.appbar.synth_button, text="Start Digital Synthesis tool")
+        tooltip.ToolTip(self.toppane.appbar.padframeCalc_button, text="Start Pad Frame Generator")
+
+        #---------------------------------------------
+        ttk.Separator(self.toppane, orient='horizontal').grid(row = 6, sticky = 'news')
+        #---------------------------------------------
+        # List of IP libraries:
+        self.toppane.library_frame = ttk.Frame(self.toppane)
+        self.toppane.library_frame.grid(row = 7, sticky = 'news')
+
+        self.toppane.library_frame.library_header = ttk.Label(self.toppane.library_frame, text='IP Library:',
+			style='title.TLabel')
+        self.toppane.library_frame.library_header.pack(side = 'left', padx = 5)
+
+        self.toppane.library_frame.library_header2 = ttk.Label(self.toppane.library_frame,
+			text='(' + self.projectdir + '/ip/)', style='normal.TLabel')
+        self.toppane.library_frame.library_header2.pack(side = 'left', padx = 5)
+
+        self.toppane.library_frame.library_header3 = ttk.Button(self.toppane.library_frame,
+		text=(allPaneOpen and '-' or '+'), command=self.library_toggle, style = 'normal.TButton', width = 2)
+        self.toppane.library_frame.library_header3.pack(side = 'right', padx = 5)
+
+        # Create listbox of IP libraries
+        iplist = self.get_library_list() if not deferLoad else []
+        height = min(8, max(iplPaneMinh, 2 + len(iplist)))
+        self.ipselect = TreeViewChoice(self.toppane, fontsize=fontsize, deferLoad=deferLoad, natSort=True)
+        self.ipselect.populate("IP Library:", iplist,
+			[], height=height, columns=[0, 1], versioning=True)
+        valuelist = self.ipselect.getvaluelist()
+        datelist = self.get_date_list(valuelist)
+        itemlist = self.ipselect.getlist()
+        self.ipselect.populate2("date", itemlist, datelist)
+        if allPaneOpen:
+            self.library_open()
+
+        #---------------------------------------------
+        ttk.Separator(self.toppane, orient='horizontal').grid(row = 9, sticky = 'news')
+        #---------------------------------------------
+        # List of imports:
+        self.toppane.import_frame = ttk.Frame(self.toppane)
+        self.toppane.import_frame.grid(row = 10, sticky = 'news')
+
+        self.toppane.import_frame.import_header = ttk.Label(self.toppane.import_frame, text='Imports:',
+			style='title.TLabel')
+        self.toppane.import_frame.import_header.pack(side = 'left', padx = 5)
+
+        self.toppane.import_frame.import_header2 = ttk.Label(self.toppane.import_frame,
+			text='(' + self.projectdir + '/import/)', style='normal.TLabel')
+        self.toppane.import_frame.import_header2.pack(side = 'left', padx = 5)
+
+        self.toppane.import_frame.import_header3 = ttk.Button(self.toppane.import_frame,
+		text=(allPaneOpen and '-' or '+'), command=self.import_toggle, style = 'normal.TButton', width = 2)
+        self.toppane.import_frame.import_header3.pack(side = 'right', padx = 5)
+
+        # Create listbox of imports
+        importlist = self.get_import_list() if not deferLoad else []
+        self.number_of_imports = len(importlist) if not deferLoad else None
+        height = min(8, max(impPaneMinh, 2 + len(importlist)))
+        self.importselect = TreeViewChoice(self.toppane, fontsize=fontsize, markDir=True, deferLoad=deferLoad)
+        self.importselect.populate("Pending Imports:", importlist,
+			[["Import As", False, self.importdesign],
+			["Import Into", False, self.importintodesign],
+			["Delete", False, self.deleteimport]], height=height, columns=[0, 1])
+        valuelist = self.importselect.getvaluelist()
+        datelist = self.get_date_list(valuelist)
+        itemlist = self.importselect.getlist()
+        self.importselect.populate2("date", itemlist, datelist)
+
+        tooltip.ToolTip(self.importselect.get_button(0), text="Import as a new project")
+        tooltip.ToolTip(self.importselect.get_button(1), text="Import into an existing project")
+        tooltip.ToolTip(self.importselect.get_button(2), text="Remove the import file(s)")
+        if allPaneOpen:
+            self.import_open()
+
+        #---------------------------------------------
+        # ttk.Separator(self, orient='horizontal').grid(column = 0, row = 8, columnspan=4, sticky='ew')
+        #---------------------------------------------
+
+        # Add a text window below the import to capture output.  Redirect
+        # print statements to it.
+        self.botpane.console = ttk.Frame(self.botpane)
+        self.botpane.console.pack(side = 'top', fill = 'both', expand = 'true')
+
+        self.text_box = ConsoleText(self.botpane.console, wrap='word', height = 4)
+        self.text_box.pack(side='left', fill='both', expand = 'true')
+        console_scrollbar = ttk.Scrollbar(self.botpane.console)
+        console_scrollbar.pack(side='right', fill='y')
+        # attach console to scrollbar
+        self.text_box.config(yscrollcommand = console_scrollbar.set)
+        console_scrollbar.config(command = self.text_box.yview)
+
+        # Give all the expansion weight to the message window.
+        # self.rowconfigure(9, weight = 1)
+        # self.columnconfigure(0, weight = 1)
+
+        # at bottom (legacy mode): window height grows by one row.
+        # at top the buttons share a row with user name, reduce window height, save screen real estate.
+        bottomButtons = False
+
+        # Add button bar: at the bottom of window (legacy mode), or share top row with user-name
+        if bottomButtons:
+            bbar =  ttk.Frame(self.botpane)
+            bbar.pack(side='top', fill = 'x')
+        else:
+            bbar =  self.toppane.user_frame
+
+        # Define help button
+        bbar.help_button = ttk.Button(bbar, text='Help',
+		                      command=self.help.open, style = 'normal.TButton')
+
+        # Define profile settings button
+        bbar.profile_button = ttk.Button(bbar, text='Settings',
+		                         command=self.profile.open, style = 'normal.TButton')
+
+        # Define the "quit" button and action
+        bbar.quit_button = ttk.Button(bbar, text='Quit', command=self.on_quit,
+                                      style = 'normal.TButton')
+        # Tool tips for button bar
+        tooltip.ToolTip(bbar.quit_button, text="Exit the project manager")
+        tooltip.ToolTip(bbar.help_button, text="Show help window")
+
+        if bottomButtons:
+            bbar.help_button.pack(side = 'left', padx = 5)
+            bbar.profile_button.pack(side = 'left', padx = 5)
+            bbar.quit_button.pack(side = 'right', padx = 5)
+        else:
+            # quit at TR like window-title's close; help towards the outside, settings towards inside
+            bbar.quit_button.pack(side = 'right', padx = 5)
+            bbar.help_button.pack(side = 'right', padx = 5)
+            bbar.profile_button.pack(side = 'right', padx = 5)
+
+        # Add the panes once the internal geometry is known
+        pane.add(self.toppane)
+        pane.add(self.botpane)
+        pane.paneconfig(self.toppane, stretch='first')
+        # self.update_idletasks()
+
+        #---------------------------------------------------------------
+        # Project list
+        # projects = os.listdir(os.path.expanduser('~/' + designdir))
+        # self.cur_project.set(projects[0])
+        # self.design_select = ttk.OptionMenu(self, self.cur_project, projects[0], *projects,
+        #		style='blue.TMenubutton')
+
+        # New import list
+        # self.import_select = ttk.Button(self, text=self.cur_import, command=self.choose_import)
+
+        #---------------------------------------------------------
+        # Define project design actions
+        # self.design_actions = ttk.Frame(self)
+        # self.design_actions.characterize = ttk.Button(self.design_actions,
+        #  	text='Upload and Characterize', command=self.characterize)
+        # self.design_actions.characterize.grid(column = 0, row = 0)
+
+        # Define import actions
+        # self.import_actions = ttk.Frame(self)
+        # self.import_actions.upload = ttk.Button(self.import_actions,
+        #	text='Upload Challenge', command=self.make_challenge)
+        # self.import_actions.upload.grid(column = 0, row = 0)
+
+        self.watchclock = WatchClock(self, watchlist, self.update_project_views, 2000,
+                                     0 if deferLoad else None) # do immediate forced refresh (1st in mainloop)
+        # self.watchclock = WatchClock(self, watchlist, self.update_project_views, 2000)
+
+        # Redirect stdout and stderr to the console as the last thing to do. . .
+        # Otherwise errors in the GUI get sucked into the void.
+        self.stdout = sys.stdout
+        self.stderr = sys.stderr
+        sys.stdout = ConsoleText.StdoutRedirector(self.text_box)
+        sys.stderr = ConsoleText.StderrRedirector(self.text_box)
+
+        if message:
+            print(message)
+
+        if self.prefs == {}:
+            print("No user preferences file, using default settings.")
+
+    # helper for Profile to do live mods of some of the user-prefs (without restart projectManager):
+    def setUsername(self, newname):
+        self.toppane.user_frame.user.config(text=newname)
+        
+    def refreshToolTips(self):
+        if self.prefs['schemeditor'] == 'xcircuit':
+            tooltip.ToolTip(self.toppane.appbar.schem_button, text="Start 'XCircuit' schematic editor")
+        elif self.prefs['schemeditor'] == 'xschem':
+            tooltip.ToolTip(self.toppane.appbar.schem_button, text="Start 'XSchem' schematic editor")
+        else:
+            tooltip.ToolTip(self.toppane.appbar.schem_button, text="Start 'Electric' schematic editor")
+
+        if self.prefs['layouteditor'] == 'klayout':
+            tooltip.ToolTip(self.toppane.appbar.layout_button, text="Start 'KLayout' layout editor")
+        else:
+            tooltip.ToolTip(self.toppane.appbar.layout_button, text="Start 'Magic' layout editor")
+            
+    def config_path(self, path):
+        #returns the directory that path contains between .config and .ef-config
+        if (os.path.exists(path + '/.config')):
+            return '/.config'
+        elif (os.path.exists(path + '/.ef-config')):
+            return '/.ef-config'
+        raise FileNotFoundError('Neither '+path+'/.config nor '+path+'/.ef-config exists.')
+
+    #------------------------------------------------------------------------
+    # Check if a name is blacklisted for being a project folder
+    #------------------------------------------------------------------------
+
+    def blacklisted(self, dirname):
+        # Blacklist:  Do not show files of these names:
+        blacklist = [importdir, 'ip', 'upload', 'export', 'lost+found']
+        if dirname in blacklist:
+            return True
+        else:
+            return False
+
+    def write_prefs(self):
+        global prefsfile
+
+        if self.prefs:
+            expprefsfile = os.path.expanduser(prefsfile)
+            prefspath = os.path.split(expprefsfile)[0]
+            if not os.path.exists(prefspath):
+                os.makedirs(prefspath)
+            with open(os.path.expanduser(prefsfile), 'w') as f:
+                json.dump(self.prefs, f, indent = 4)
+
+    def read_prefs(self):
+        global prefsfile
+
+        # Set all known defaults even if they are not in the JSON file so
+        # that it is not necessary to check for the existence of the keyword
+        # in the dictionary every time it is accessed.
+        if 'fontsize' not in self.prefs:
+            self.prefs['fontsize'] = 11
+        userid = os.environ['USER']
+        uid = ''
+        username = userid
+        self.prefs['username'] = username
+        
+        '''
+        if 'username' not in self.prefs:
+            
+            # 
+            #EFABLESS PLATFORM
+            p = subprocess.run(['/ef/apps/bin/withnet' ,
+			og_config.apps_path + '/og_uid_service.py', userid],
+			stdout = subprocess.PIPE)
+            if p.stdout:
+                uid_string = p.stdout.splitlines()[0].decode('utf-8')
+                userspec = re.findall(r'[^"\s]\S*|".+?"', uid_string)
+                if len(userspec) > 0:
+                    username = userspec[0].strip('"')
+                    # uid = userspec[1]
+                    # Note userspec[1] = UID and userspec[2] = role, useful
+                    # for future applications.
+                else:
+                    username = userid
+            else:
+                username = userid
+            self.prefs['username'] = username
+            # self.prefs['uid'] = uid
+        '''
+        if 'schemeditor' not in self.prefs:
+            self.prefs['schemeditor'] = 'electric'
+
+        if 'layouteditor' not in self.prefs:
+            self.prefs['layouteditor'] = 'magic'
+
+        if 'magic-graphics' not in self.prefs:
+            self.prefs['magic-graphics'] = 'X11'
+
+        if 'development' not in self.prefs:
+            self.prefs['development'] = False
+
+        if 'devstdcells' not in self.prefs:
+            self.prefs['devstdcells'] = False
+
+        # Any additional user preferences go above this line.
+
+        # Get user preferences from ~/design/.profile/prefs.json and use it to
+        # overwrite default entries in self.prefs
+        try:
+            with open(os.path.expanduser(prefsfile), 'r') as f:
+                prefsdict = json.load(f)
+                for key in prefsdict:
+                    self.prefs[key] = prefsdict[key]
+        except:
+            # No preferences file, so create an initial one.
+            if not os.path.exists(prefsfile):
+                self.write_prefs()
+        
+        # if 'User:' Label exists, this updates it live (Profile calls read_prefs after write)
+        try:
+            self.setUsername(self.prefs['username'])
+        except:
+            pass
+
+    #------------------------------------------------------------------------
+    # Get a list of the projects in the user's design directory.  Exclude
+    # items that are not directories, or which are blacklisted.
+    #------------------------------------------------------------------------
+
+    def get_project_list(self):
+        global importdir
+
+        badrex1 = re.compile("^\.")
+        badrex2 = re.compile(".*[ \t\n].*")
+
+        # Get contents of directory.  Look only at directories
+        projectlist = list(item for item in os.listdir(self.projectdir) if
+			os.path.isdir(self.projectdir + '/' + item))
+
+        # 'import' and others in the blacklist are not projects!
+        # Files beginning with '.' and files with whitespace are
+        # also not listed.
+        for item in projectlist[:]:
+            if self.blacklisted(item):
+                projectlist.remove(item)
+            elif badrex1.match(item):
+                projectlist.remove(item)
+            elif badrex2.match(item):
+                projectlist.remove(item)
+
+        # Add pathname to all items in projectlist
+        projectlist = [self.projectdir + '/' + item for item in projectlist]
+        return projectlist
+
+    #------------------------------------------------------------------------
+    # Get a list of the projects in the user's cloudv directory.  Exclude
+    # items that are not directories, or which are blacklisted.
+    #------------------------------------------------------------------------
+
+    def get_cloudv_project_list(self):
+        global importdir
+
+        badrex1 = re.compile("^\.")
+        badrex2 = re.compile(".*[ \t\n].*")
+
+        if not os.path.exists(self.cloudvdir):
+            print('No user cloudv dir exists;  no projects to import.')
+            return None
+
+        # Get contents of cloudv directory.  Look only at directories
+        projectlist = list(item for item in os.listdir(self.cloudvdir) if
+			os.path.isdir(self.cloudvdir + '/' + item))
+
+        # 'import' and others in the blacklist are not projects!
+        # Files beginning with '.' and files with whitespace are
+        # also not listed.
+        for item in projectlist[:]:
+            if self.blacklisted(item):
+                projectlist.remove(item)
+            elif badrex1.match(item):
+                projectlist.remove(item)
+            elif badrex2.match(item):
+                projectlist.remove(item)
+
+        # Add pathname to all items in projectlist
+        projectlist = [self.cloudvdir + '/' + item for item in projectlist]
+        return projectlist
+
+    #------------------------------------------------------------------------
+    # utility: [re]intialize a project's elec/ dir: the .java preferences and LIBDIRS.
+    # So user can just delete .java, and restart electric (from projectManager), to reinit preferences.
+    # So user can just delete LIBDIRS, and restart electric (from projectManager), to reinit LIBDIRS.
+    # So project copies/imports can filter ngspice/run (and ../.allwaves), we'll recreate it here.
+    #
+    # The global /ef/efabless/deskel/* is used and the PDK name substituted.
+    #
+    # This SINGLE function is used to setup elec/ contents for new projects, in addition to being
+    # called in-line prior to "Edit Schematics" (on-the-fly).
+    #------------------------------------------------------------------------
+    @classmethod
+    def reinitElec(cls, design):
+        pdkdir = os.path.join( design, ".ef-config/techdir")
+        elec = os.path.join( design, "elec")
+
+        # on the fly, ensure has elec/ dir, ensure has ngspice/run/allwaves dir
+        try:
+            os.makedirs(design + '/elec', exist_ok=True)
+        except IOError as e:
+            print('Error in os.makedirs(elec): ' + str(e))
+        try:
+            os.makedirs(design + '/ngspice/run/.allwaves', exist_ok=True)
+        except IOError as e:
+            print('Error in os.makedirs(.../.allwaves): ' + str(e))
+        #EFABLESS PLATFORM
+        deskel = '/ef/efabless/deskel'
+        
+        # on the fly:
+        # .../elec/.java : reinstall if missing. From PDK-specific if any.
+        if not os.path.exists( os.path.join( elec, '.java')):
+            # Copy Electric preferences
+            try:
+                shutil.copytree(deskel + '/dotjava', design + '/elec/.java', symlinks = True)
+            except IOError as e:
+                print('Error copying files: ' + str(e))
+
+        # .../elec/LIBDIRS : reinstall if missing, from PDK-specific LIBDIRS
+        # in libs.tech/elec/LIBDIRS
+
+        libdirsloc = pdkdir + '/libs.tech/elec/LIBDIRS'
+
+        if not os.path.exists( os.path.join( elec, 'LIBDIRS')):
+            if os.path.exists( libdirsloc ):
+                # Copy Electric LIBDIRS
+                try:
+                    shutil.copy(libdirsloc, design + '/elec/LIBDIRS')
+                except IOError as e:
+                    print('Error copying files: ' + str(e))
+            else:
+                print('Info: PDK not configured for Electric: no libs.tech/elec/LIBDIRS')
+
+        return None
+
+    #------------------------------------------------------------------------
+    # utility: filter a list removing: empty strings, strings with any whitespace
+    #------------------------------------------------------------------------
+    whitespaceREX = re.compile('\s')
+    @classmethod
+    def filterNullOrWS(cls, inlist):
+        return [ i for i in inlist if i and not cls.whitespaceREX.search(i) ]
+
+    #------------------------------------------------------------------------
+    # utility: do a glob.glob of relative pattern, but specify the rootDir,
+    # so returns the matching paths found below that rootDir.
+    #------------------------------------------------------------------------
+    @classmethod
+    def globFromDir(cls, pattern, dir=None):
+        if dir:
+            dir = dir.rstrip('/') + '/'
+            pattern = dir + pattern
+        result = glob.glob(pattern)
+        if dir and result:
+            nbr = len(dir)
+            result = [ i[nbr:] for i in result ]
+        return result
+
+    #------------------------------------------------------------------------
+    # utility: from a pdkPath, return list of 3 strings: <foundry>, <node>, <description>.
+    # i.e. pdkPath has form '[.../]<foundry>[.<ext>]/<node>'. For now the description
+    # is always ''. And an optional foundry extension is pruned/dropped.
+    # thus '.../XFAB.2/EFXP018A4' -> 'XFAB', 'EFXP018A4', ''
+    #
+    # optionally store in each PDK: .ef-config/nodeinfo.json which can define keys:
+    # 'foundry', 'node', 'description' to override the foundry (computed from the path)
+    # and (fixed, empty) description currently returned by this.
+    #
+    # Intent: keep a short-description field at least, intended to be one-line max 40 chars,
+    # suitable for a on-hover-tooltip display. (Distinct from a big multiline description).
+    #
+    # On error (malformed pdkPath: can't determine foundry or node), the foundry or node
+    # or both may be '' or as specified in the optional default values (if you're
+    # generating something for display and want an unknown to appear as 'none' etc.).
+    #------------------------------------------------------------------------
+    @classmethod
+    def pdkdir2fnd(cls, pdkdir, def_foundry='', def_node='', def_description=''):
+        foundry = ''
+        node = ''
+        description = ''
+        status = 'active'
+        if pdkdir:
+            split = os.path.split(os.path.realpath(pdkdir))
+            # Full path should be [<something>/]<foundry>[.ext]/<node>
+            node = split[1]
+            foundry = os.path.split(split[0])[1]
+            foundry = os.path.splitext(foundry)[0]
+            # Check for nodeinfo.json
+            infofile = pdkdir + '/.config/nodeinfo.json'
+            if os.path.exists(infofile):
+                with open(infofile, 'r') as ifile:
+                    nodeinfo = json.load(ifile)
+                if 'foundry' in nodeinfo:
+                    foundry = nodeinfo['foundry']
+                if 'node' in nodeinfo:
+                    node = nodeinfo['node']
+                if 'description' in nodeinfo:
+                    description = nodeinfo['description']
+                if 'status' in nodeinfo:
+                    status = nodeinfo['status']
+                return foundry, node, description, status
+            
+            infofile = pdkdir + '/.ef-config/nodeinfo.json'
+            if os.path.exists(infofile):
+                with open(infofile, 'r') as ifile:
+                    nodeinfo = json.load(ifile)
+                if 'foundry' in nodeinfo:
+                    foundry = nodeinfo['foundry']
+                if 'node' in nodeinfo:
+                    node = nodeinfo['node']
+                if 'description' in nodeinfo:
+                    description = nodeinfo['description']
+                if 'status' in nodeinfo:
+                    status = nodeinfo['status']
+            
+
+        return foundry, node, description, status
+
+    #------------------------------------------------------------------------
+    # Get a list of the electric-libraries (DELIB only) in a given project.
+    # List of full-paths each ending in '.delib'
+    #------------------------------------------------------------------------
+
+    def get_elecLib_list(self, pname):
+        elibs = self.globFromDir(pname + '/elec/*.delib/', self.projectdir)
+        elibs = [ re.sub("/$", "", i) for i in elibs ]
+        return self.filterNullOrWS(elibs)
+
+    #------------------------------------------------------------------------
+    # Create a list of datestamps for each import file
+    #------------------------------------------------------------------------
+    def get_date_list(self, valuelist):
+        datelist = []
+        for value in valuelist:
+            try:
+                importfile = value[0]
+                try:
+                    statbuf = os.stat(importfile)
+                except:
+                    # Note entries that can't be accessed.
+                    datelist.append("(unknown)")
+                else:
+                    datestamp = datetime.datetime.fromtimestamp(statbuf.st_mtime)
+                    datestr = datestamp.strftime("%c")
+                    datelist.append(datestr)
+            except:
+                datelist.append("(N/A)")
+
+        return datelist
+
+    #------------------------------------------------------------------------
+    # Get the PDK attached to a project for display as: '<foundry> : <node>'
+    # unless path=True: then return true PDK dir-path.
+    #
+    # TODO: the ef-config prog output is not used below. Intent was use
+    # ef-config to be the one official query for *any* project's PDK value, and
+    # therein-only hide a built-in default for legacy projects without techdir symlink.
+    # In below ef-config will always give an EF_TECHDIR, so that code-branch always
+    # says '(default)', the ef-config subproc is wasted, and '(no PDK)' is never
+    # reached.
+    #------------------------------------------------------------------------
+    def get_pdk_dir(self, project, path=False):
+        pdkdir = os.path.realpath(project + self.config_path(project)+'/techdir')
+        if path:
+            return pdkdir
+        foundry, node, desc, status = self.pdkdir2fnd( pdkdir )
+        return foundry + ' : ' + node
+        '''
+        if os.path.isdir(project + '/.ef-config'):
+            if os.path.exists(project + '/.ef-config/techdir'):
+                pdkdir = os.path.realpath(project + '/.ef-config/techdir')
+                
+        elif os.path.isdir(project + '/.config'):
+            if os.path.exists(project + '/.config/techdir'):
+                pdkdir = os.path.realpath(project + '/.config/techdir')
+                if path:
+                    return pdkdir
+                foundry, node, desc, status = self.pdkdir2fnd( pdkdir )
+                return foundry + ' : ' + node
+        '''
+        '''
+        if not pdkdir:
+            # Run "ef-config" script for backward compatibility
+            export = {'EF_DESIGNDIR': project}
+            #EFABLESS PLATFORM
+            p = subprocess.run(['/ef/efabless/bin/ef-config', '-sh', '-t'],
+			stdout = subprocess.PIPE, env = export)
+            config_out = p.stdout.splitlines()
+            for line in config_out:
+                setline = line.decode('utf-8').split('=')
+                if setline[0] == 'EF_TECHDIR':
+                    pdkdir = ( setline[1] if path else '(default)' )
+            if not pdkdir:
+                pdkdir = ( None if path else '(no PDK)' )    # shouldn't get here
+        '''
+        
+        
+
+        return pdkdir
+
+    #------------------------------------------------------------------------
+    # Get the list of PDKs that are attached to each project
+    #------------------------------------------------------------------------
+    def get_pdk_list(self, projectlist):
+        pdklist = []
+        for project in projectlist:
+            pdkdir = self.get_pdk_dir(project)
+            pdklist.append(pdkdir)
+
+        return pdklist
+
+    #------------------------------------------------------------------------
+    # Find a .json's associated tar.gz (or .tgz) if any.
+    # Return path to the tar.gz if any, else None.
+    #------------------------------------------------------------------------
+
+    def json2targz(self, jsonPath):
+        root = os.path.splitext(jsonPath)[0]
+        for ext in ('.tgz', '.tar.gz'):
+            if os.path.isfile(root + ext):
+                return root + ext
+        return None
+
+    #------------------------------------------------------------------------
+    # Remove a .json and associated tar.gz (or .tgz) if any.
+    # If not a .json, remove just that file (no test for a tar).
+    #------------------------------------------------------------------------
+
+    def removeJsonPlus(self, jsonPath):
+        ext = os.path.splitext(jsonPath)[1]
+        if ext == ".json":
+            tar = self.json2targz(jsonPath)
+            if tar: os.remove(tar)
+        return os.remove(jsonPath)
+
+    #------------------------------------------------------------------------
+    # MOVE a .json and associated tar.gz (or .tgz) if any, to targetDir.
+    # If not a .json, move just that file (no test for a tar).
+    #------------------------------------------------------------------------
+
+    def moveJsonPlus(self, jsonPath, targetDir):
+        ext = os.path.splitext(jsonPath)[1]
+        if ext == ".json":
+            tar = self.json2targz(jsonPath)
+            if tar:
+                shutil.move(tar,      targetDir)
+        # believe the move throws an error. So return value (the targetDir name) isn't really useful.
+        return  shutil.move(jsonPath, targetDir)
+
+    #------------------------------------------------------------------------
+    # Get a list of the libraries in the user's ip folder
+    #------------------------------------------------------------------------
+
+    def get_library_list(self):
+        # Get contents of directory
+        try:
+            iplist = glob.glob(self.projectdir + '/ip/*/*')
+        except:
+            iplist = []
+        else:
+            pass
+      
+        return iplist
+
+    #------------------------------------------------------------------------
+    # Get a list of the files in the user's design import folder
+    # (use current 'import' but also original 'upload')
+    #------------------------------------------------------------------------
+
+    def get_import_list(self):
+        # Get contents of directory
+        importlist = os.listdir(self.projectdir + '/' + importdir)
+
+        # If entries have both a .json and .tar.gz file, remove the .tar.gz (also .tgz).
+        # Also ignore any .swp files dropped by the vim editor.
+        # Also ignore any subdirectories of import
+        for item in importlist[:]:
+            if item[-1] in '#~':
+                importlist.remove(item)
+                continue
+            ipath = self.projectdir + '/' + importdir + '/' + item
+
+            # recognize dirs (as u2u projects) if not symlink and has a 'project.json',
+            # hide dirs named *.bak. If originating user does u2u twice before target user
+            # can consume/import it, the previous one (only) is retained as *.bak.
+            if os.path.isdir(ipath):
+                if os.path.islink(ipath) or not self.validProjectName(item) \
+                   or self.importProjNameBadrex1.match(item) \
+                   or not os.path.isfile(ipath + '/project.json'):
+                    importlist.remove(item)
+                    continue
+            else:
+                ext = os.path.splitext(item)
+                if ext[1] == '.json':
+                    if ext[0] + '.tar.gz' in importlist:
+                        importlist.remove(ext[0] + '.tar.gz')
+                    elif ext[0] + '.tgz' in importlist:
+                        importlist.remove(ext[0] + '.tgz')
+                elif ext[1] == '.swp':
+                    importlist.remove(item)
+                elif os.path.isdir(self.projectdir + '/' + importdir + '/' + item):
+                    importlist.remove(item)
+
+        # Add pathname to all items in projectlist
+        importlist = [self.projectdir + '/' + importdir + '/' + item for item in importlist]
+
+        # Add support for original "upload" directory (backward compatibility)
+        if os.path.exists(self.projectdir + '/upload'):
+            uploadlist = os.listdir(self.projectdir + '/upload')
+
+            # If entries have both a .json and .tar.gz file, remove the .tar.gz (also .tgz).
+            # Also ignore any .swp files dropped by the vim editor.
+            for item in uploadlist[:]:
+                ext = os.path.splitext(item)
+                if ext[1] == '.json':
+                    if ext[0] + '.tar.gz' in uploadlist:
+                        uploadlist.remove(ext[0] + '.tar.gz')
+                    elif ext[0] + '.tgz' in uploadlist:
+                        uploadlist.remove(ext[0] + '.tgz')
+                elif ext[1] == '.swp':
+                    uploadlist.remove(item)
+
+            # Add pathname to all items in projectlist
+            uploadlist = [self.projectdir + '/upload/' + item for item in uploadlist]
+            importlist.extend(uploadlist)
+
+        # Remember the size of the list so we know when it changed
+        self.number_of_imports = len(importlist)
+        return importlist 
+
+    #------------------------------------------------------------------------
+    # Import for json documents and related tarballs (.gz or .tgz):  
+    #------------------------------------------------------------------------
+
+    def importjson(self, projname, importfile):
+        # (1) Check if there is a tarball with the same root name as the JSON
+        importroot = os.path.splitext(importfile)[0]
+        badrex1 = re.compile("^\.")
+        badrex2 = re.compile(".*[/ \t\n\\\><\*\?].*")
+        if os.path.isfile(importroot + '.tgz'):
+           tarname = importroot + '.tgz'
+        elif os.path.isfile(importroot + '.tar.gz'):
+           tarname = importroot + '.tar.gz'
+        else:
+           tarname = []
+        # (2) Check for name conflict
+        origname = projname
+        newproject = self.projectdir + '/' + projname
+        newname = projname
+        while os.path.isdir(newproject) or self.blacklisted(newname):
+            if self.blacklisted(newname):
+                warning = "Name " + newname + " is not allowed for a project name."
+            elif badrex1.match(newname):
+                warning = 'project name may not start with "."'
+            elif badrex2.match(newname):
+                warning = 'project name contains illegal characters or whitespace.'
+            else:
+                warning = "Project " + newname + " already exists!"
+            newname = ProjectNameDialog(self, warning, seed=newname).result
+            if not newname:
+                return 0	# Canceled, no action.
+            newproject = self.projectdir + '/' + newname
+        print("New project name is " + newname + ".")
+        # (3) Create new directory
+        os.makedirs(newproject)
+        # (4) Dump the tarball (if any) in the new directory
+        if tarname:
+            with tarfile.open(tarname, mode='r:gz') as archive:
+                for member in archive:
+                    archive.extract(member, newproject)
+        # (5) Copy the JSON document into the new directory.  Keep the
+        # original name of the project, so as to overwrite any existing
+        # document, then change the name to match that of the project
+        # folder.
+        # New behavior 12/2018:  JSON file is always called 'project.json'.
+        # Also support legacy JSON name if it exists (don't generate files with
+        # both names)
+
+        jsonfile = newproject + '/project.json'
+        if not os.path.isfile(jsonfile):
+            if os.path.isfile(newproject + '/' + projname + '.json'):
+                jsonfile = newproject + '/' + projname + '.json'
+
+        try:
+            shutil.copy(importfile, jsonfile)
+        except IOError as e:
+            print('Error copying files: ' + str(e))
+            return None
+        else:
+            # If filename is 'project.json' then it does not need to be changed.
+            # This is for legacy name support only.
+            if jsonfile != newproject + '/project.json':
+                shutil.move(jsonfile, newproject + '/' + newname + '.json')
+
+        # (6) Remove the original files from the import folder
+        os.remove(importfile)
+        if tarname:
+            os.remove(tarname)
+
+        # (7) Standard project setup:  if spi/, elec/, and ngspice/ do not
+        # exist, create them.  If elec/.java does not exist, create it and
+        # seed from deskel.  If ngspice/run and ngspice/run/.allwaves do not
+        # exist, create them.
+
+        if not os.path.exists(newproject + '/spi'):
+            os.makedirs(newproject + '/spi')
+        if not os.path.exists(newproject + '/spi/pex'):
+            os.makedirs(newproject + '/spi/pex')
+        if not os.path.exists(newproject + '/spi/lvs'):
+            os.makedirs(newproject + '/spi/lvs')
+        if not os.path.exists(newproject + '/ngspice'):
+            os.makedirs(newproject + '/ngspice')
+        if not os.path.exists(newproject + '/ngspice/run'):
+            os.makedirs(newproject + '/ngspice/run')
+        if not os.path.exists(newproject + '/ngspice/run/.allwaves'):
+            os.makedirs(newproject + '/ngspice/run/.allwaves')
+        if not os.path.exists(newproject + '/elec'):
+            os.makedirs(newproject + '/elec')
+        if not os.path.exists(newproject + '/xcirc'):
+            os.makedirs(newproject + '/xcirc')
+        if not os.path.exists(newproject + '/mag'):
+            os.makedirs(newproject + '/mag')
+
+        self.reinitElec(newproject)   # [re]install elec/.java, elec/LIBDIRS if needed, from pdk-specific if-any
+
+        return 1	# Success
+
+    #------------------------------------------------------------------------
+    # Import for netlists (.spi):
+    # (1) Request project name
+    # (2) Create new project if name does not exist, or
+    #     place netlist in existing project if it does.
+    #------------------------------------------------------------------------
+
+    #--------------------------------------------------------------------
+    # Install netlist in electric:
+    # "importfile" is the filename in ~/design/import
+    # "pname" is the name of the target project (folder)
+    # "newfile" is the netlist file name (which may or may not be the same
+    #     as 'importfile').
+    #--------------------------------------------------------------------
+
+    def install_in_electric(self, importfile, pname, newfile, isnew=True):
+        #--------------------------------------------------------------------
+        # Install the netlist.
+        # If netlist is CDL, then call cdl2spi first
+        #--------------------------------------------------------------------
+
+        newproject = self.projectdir + '/' + pname
+        if not os.path.isdir(newproject + '/spi/'):
+            os.makedirs(newproject + '/spi/')
+        if os.path.splitext(newfile)[1] == '.cdl':
+            if not os.path.isdir(newproject + '/cdl/'):
+                os.makedirs(newproject + '/cdl/')
+            shutil.copy(importfile, newproject + '/cdl/' + newfile)
+            try:
+                p = subprocess.run(['/ef/apps/bin/cdl2spi', importfile],
+			stdout = subprocess.PIPE, stderr = subprocess.PIPE,
+			check = True)
+            except subprocess.CalledProcessError as e:
+                print('Error running cdl2spi: ' + e.output.decode('utf-8'))
+                if isnew == True:
+                    shutil.rmtree(newproject)
+                return None
+            else:
+                spi_string = p.stdout.splitlines()[0].decode('utf-8')
+                if p.stderr:
+                    err_string = p.stderr.splitlines()[0].decode('utf-8')
+                    # Print error messages to console
+                    print(err_string)
+            if not spi_string:
+                print('Error: cdl2spi has no output')
+                if isnew == True:
+                    shutil.rmtree(newproject)
+                return None
+            outname = os.path.splitext(newproject + '/spi/' + newfile)[0] + '.spi'
+            with open(outname, 'w') as f:
+                f.write(spi_string)
+        else:
+            outname = newproject + '/spi/' + newfile
+            try:
+                shutil.copy(importfile, outname)
+            except IOError as e:
+                print('Error copying files: ' + str(e))
+                if isnew == True:
+                    shutil.rmtree(newproject)
+                return None
+
+        #--------------------------------------------------------------------
+        # Symbol generator---this code to be moved into its own def.
+        #--------------------------------------------------------------------
+        # To-do, need a more thorough SPICE parser, maybe use netgen to parse.
+        # Need to find topmost subcircuit, by parsing the hieararchy.
+        subcktrex = re.compile('\.subckt[ \t]+([^ \t]+)[ \t]+', re.IGNORECASE)
+        subnames = []
+        with open(importfile, 'r') as f:
+            for line in f:
+                lmatch = subcktrex.match(line)
+                if lmatch:
+                    subnames.append(lmatch.group(1))
+
+        if subnames:
+            subname = subnames[0]
+
+        # Run cdl2icon perl script
+        try:
+            p = subprocess.run(['/ef/apps/bin/cdl2icon', '-file', importfile, '-cellname',
+			subname, '-libname', pname, '-projname', pname, '--prntgussddirs'],
+			stdout = subprocess.PIPE, stderr = subprocess.PIPE, check = True)
+        except subprocess.CalledProcessError as e:
+            print('Error running cdl2spi: ' + e.output.decode('utf-8'))
+            return None
+        else:
+            pin_string = p.stdout.splitlines()[0].decode('utf-8')
+            if not pin_string:
+                print('Error: cdl2icon has no output')
+                if isnew == True:
+                    shutil.rmtree(newproject)
+                return None
+            if p.stderr:
+                err_string = p.stderr.splitlines()[0].decode('utf-8')
+                print(err_string)
+
+        # Invoke dialog to arrange pins here
+        pin_info_list = SymbolBuilder(self, pin_string.split(), fontsize=self.prefs['fontsize']).result
+        if not pin_info_list:
+            # Dialog was canceled
+            print("Symbol builder was canceled.")
+            if isnew == True:
+                shutil.rmtree(newproject)
+            return 0
+
+        for pin in pin_info_list:
+            pin_info = pin.split(':')
+            pin_name = pin_info[0]
+            pin_type = pin_info[1]
+
+        # Call cdl2icon with the final pin directions
+        outname = newproject + '/elec/' + pname + '.delib/' + os.path.splitext(newfile)[0] + '.ic'
+        try:
+            p = subprocess.run(['/ef/apps/bin/cdl2icon', '-file', importfile, '-cellname',
+			subname, '-libname', pname, '-projname', pname, '-output',
+			outname, '-pindircmbndstring', ','.join(pin_info_list)],
+			stdout = subprocess.PIPE, stderr = subprocess.PIPE, check = True)
+        except subprocess.CalledProcessError as e:
+            print('Error running cdl2icon: ' + e.output.decode('utf-8'))
+            if isnew == True:
+                shutil.rmtree(newproject)
+            return None
+        else:
+            icon_string = p.stdout.splitlines()[0].decode('utf-8')   # not used, AFAIK
+            if p.stderr:
+                err_string = p.stderr.splitlines()[0].decode('utf-8')
+                print(err_string)
+
+        return 1	# Success
+
+    #------------------------------------------------------------------------
+    # Import netlist file into existing project
+    #------------------------------------------------------------------------
+
+    def importspiceinto(self, newfile, importfile):
+        # Require existing project location
+        ppath = ExistingProjectDialog(self, self.get_project_list()).result
+        if not ppath:
+            return 0		# Canceled in dialog, no action.
+        pname = os.path.split(ppath)[1]
+        print("Importing into existing project " + pname)
+        result = self.install_in_electric(importfile, pname, newfile, isnew=False)
+        if result == None:
+            print('Error during import.')
+            return None
+        elif result == 0:
+            return 0    # Canceled
+        else:
+            # Remove original file from imports area
+            os.remove(importfile)
+            return 1    # Success
+
+    #------------------------------------------------------------------------
+    # Import netlist file as a new project
+    #------------------------------------------------------------------------
+
+    def importspice(self, newfile, importfile):
+        # Use create project code first to generate a valid project space.
+        newname = self.createproject(None)
+        if not newname:
+            return 0		# Canceled in dialog, no action.
+        print("Importing as new project " + newname + ".")
+        result = self.install_in_electric(importfile, newname, newfile, isnew=True)
+        if result == None:
+            print('Error during install')
+            return None
+        elif result == 0: 
+            # Canceled, so do not remove the import
+            return 0
+        else: 
+            # Remove original file from imports area
+            os.remove(importfile)
+            return 1    # Success
+
+    #------------------------------------------------------------------------
+    # Determine if JSON's tar can be imported as-if it were just a *.v.
+    # This is thin wrapper around tarVglImportable. Find the JSON's associated
+    # tar.gz if any, and call tarVglImportable.
+    # Returns list of two:
+    #   None if rules not satisified; else path of the single GL .v member.
+    #   None if rules not satisified; else root-name of the single .json member.
+    #------------------------------------------------------------------------
+
+    def jsonTarVglImportable(self, path):
+        ext = os.path.splitext(path)[1]
+        if ext != '.json': return None, None, None
+
+        tar = self.json2targz(path)
+        if not tar: return None, None, None
+
+        return self.tarVglImportable(tar)
+
+    #------------------------------------------------------------------------
+    # Get a single named member (memPath) out of a JSON's tar file.
+    # This is thin wrapper around tarMember2tempfile. Find the JSON's associated
+    # tar.gz if any, and call tarMember2tempfile.
+    #------------------------------------------------------------------------
+
+    def jsonTarMember2tempfile(self, path, memPath):
+        ext = os.path.splitext(path)[1]
+        if ext != '.json': return None
+
+        tar = self.json2targz(path)
+        if not tar: return None
+
+        return self.tarMember2tempfile(tar, memPath)
+
+    #------------------------------------------------------------------------
+    # Determine if tar-file can be imported as-if it were just a *.v.
+    # Require exactly one yosys-output .netlist.v, and exactly one .json.
+    # Nothing else matters: Ignore all other *.v, *.tv, *.jelib, *.vcd...
+    #
+    # If user renames *.netlist.v in cloudv before export to not end in
+    # netlist.v, we won't recognize it.
+    #
+    # Returns list of two:
+    #   None if rules not satisified; else path of the single GL netlist.v member.
+    #   None if rules not satisified; else root-name of the single .json member.
+    #------------------------------------------------------------------------
+
+    def tarVglImportable(self, path):
+        # count tar members by extensions. Track the .netlist.v. and .json. Screw the rest.
+        nbrExt = {'.v':0, '.netlist.v':0, '.tv':0, '.jelib':0, '.json':0, '/other/':0, '/vgl/':0}
+        nbrGLv = 0
+        jname = None
+        vfile = None
+        node = None
+        t = tarfile.open(path)
+        for i in t:
+            # ignore (without counting) dir entries. From cloudv (so far) the tar does not
+            # have dir-entries, but most tar do (esp. most manually made test cases).
+            if i.isdir():
+                continue
+            # TODO: should we require all below counted files to be plain files (no symlinks etc.)?
+            # get extension, but recognize a multi-ext for .netlist.v case
+            basenm = os.path.basename(i.name)
+            ext = os.path.splitext(basenm)[1]
+            root = os.path.splitext(basenm)[0]
+            ext2 = os.path.splitext(root)[1]
+            if ext2 == '.netlist' and ext == '.v':
+                ext = ext2 + ext
+            if ext and ext not in nbrExt:
+                ext = '/other/'
+            elif ext == '.netlist.v' and self.tarMemberIsGLverilog(t, i.name):
+                vfile = i.name
+                ext = '/vgl/'
+            elif ext == '.json':
+                node = self.tarMemberHasFoundryNode(t, i.name)
+                jname = root
+            nbrExt[ext] += 1
+
+        # check rules. Require exactly one yosys-output .netlist.v, and exactly one .json.
+        # Quantities of other types are all don't cares.
+        if (nbrExt['/vgl/'] == 1 and nbrExt['.json'] == 1):
+            # vfile is the name of the verilog netlist in the tarball, while jname
+            # is the root name of the JSON file found in the tarball (if any) 
+            return vfile, jname, node
+
+        # failed, not gate-level-verilog importable:
+        return None, None, node
+
+
+    #------------------------------------------------------------------------
+    # OBSOLETE VERSION: Determine if tar-file can be imported as-if it were just a *.v.
+    # Rules for members: one *.v, {0,1} *.jelib, {0,1} *.json, 0 other types.
+    # Return None if rules not satisified; else return path of the single .v.
+    #------------------------------------------------------------------------
+    #
+    # def tarVglImportable(self, path):
+    #     # count tar members by extensions. Track the .v.
+    #     nbrExt = {'.v':0, '.jelib':0, '.json':0, 'other':0}
+    #     vfile = ""
+    #     t = tarfile.open(path)
+    #     for i in t:
+    #         ext = os.path.splitext(i.name)[1]
+    #         if ext not in nbrExt:
+    #             ext = 'other'
+    #         nbrExt[ext] += 1
+    #         if ext == ".v": vfile = i.name
+    #
+    #     # check rules.
+    #     if (nbrExt['.v'] != 1 or nbrExt['other'] != 0 or
+    #         nbrExt['.jelib'] > 1 or nbrExt['.json'] > 1):
+    #         return None
+    #     return vfile
+
+    #------------------------------------------------------------------------
+    # Get a single named member (memPath) out of a tar file (tarPath), into a
+    # temp-file, so subprocesses can process it.
+    # Return path to the temp-file, or None if member not found in the tar.
+    #------------------------------------------------------------------------
+
+    def tarMember2tempfile(self, tarPath, memPath):
+        t = tarfile.open(tarPath)
+        member = t.getmember(memPath)
+        if not member: return None
+
+        # Change member.name so it extracts into our new temp-file.
+        # extract() can specify the root-dir befow which the member path
+        # resides. If temp is an absolute-path, that root-dir must be /.
+        tmpf1 = tempfile.NamedTemporaryFile(delete=False)
+        if tmpf1.name[0] != "/":
+            raise ValueError("assertion failed, temp-file path not absolute: %s" % tmpf1.name)
+        member.name = tmpf1.name
+        t.extract(member,"/")
+
+        return tmpf1.name
+
+    #------------------------------------------------------------------------
+    # Create an electric .delib directory and seed it with a header file
+    #------------------------------------------------------------------------
+
+    def create_electric_header_file(self, project, libname):
+        if not os.path.isdir(project + '/elec/' + libname + '.delib'):
+            os.makedirs(project + '/elec/' + libname + '.delib')
+
+        p = subprocess.run(['electric', '-v'], stdout=subprocess.PIPE)
+        eversion = p.stdout.splitlines()[0].decode('utf-8')
+        # Create header file
+        with open(project + '/elec/' + libname + '.delib/header', 'w') as f:
+            f.write('# header information:\n')
+            f.write('H' + libname + '|' + eversion + '\n\n')
+            f.write('# Tools:\n')
+            f.write('Ouser|DefaultTechnology()Sschematic\n')
+            f.write('Osimulation|VerilogUseAssign()BT\n')
+            f.write('C____SEARCH_FOR_CELL_FILES____\n')
+
+    #------------------------------------------------------------------------
+    # Create an ad-hoc "project.json" dictionary and fill essential records
+    #------------------------------------------------------------------------
+
+    def create_ad_hoc_json(self, ipname, pname):
+        # Create ad-hoc JSON file and fill it with the minimum
+        # necessary entries to define a project.
+        jData = {}
+        jDS = {}
+        jDS['ip-name'] = ipname
+        pdkdir = self.get_pdk_dir(pname, path=True)
+        try:
+            jDS['foundry'], jDS['node'], pdk_desc, pdk_stat = self.pdkdir2fnd( pdkdir )
+        except:
+            # Cannot parse PDK name, so foundry and node will remain undefined
+            pass
+        jDS['format'] = '3'
+        pparams = []
+        param = {}
+        param['unit'] = "\u00b5m\u00b2"
+        param['condition'] = "device_area"
+        param['display'] = "Device area"
+        pmax = {}
+        pmax['penalty'] = '0'
+        pmax['target'] = '100000'
+        param['max'] = pmax
+        pparams.append(param)
+    
+        param = {}
+        param['unit'] = "\u00b5m\u00b2"
+        param['condition'] = "area"
+        param['display'] = "Layout area"
+        pmax = {}
+        pmax['penalty'] = '0'
+        pmax['target'] = '100000'
+        param['max'] = pmax
+        pparams.append(param)
+
+        param = {}
+        param['unit'] = "\u00b5m"
+        param['condition'] = "width"
+        param['display'] = "Layout width"
+        pmax = {}
+        pmax['penalty'] = '0'
+        pmax['target'] = '300'
+        param['max'] = pmax
+        pparams.append(param)
+
+        param = {}
+        param['condition'] = "DRC_errors"
+        param['display'] = "DRC errors"
+        pmax = {}
+        pmax['penalty'] = 'fail'
+        pmax['target'] = '0'
+        param['max'] = pmax
+        pparams.append(param)
+
+        param = {}
+        param['condition'] = "LVS_errors"
+        param['display'] = "LVS errors"
+        pmax = {}
+        pmax['penalty'] = 'fail'
+        pmax['target'] = '0'
+        param['max'] = pmax
+        pparams.append(param)
+
+        jDS['physical-params'] = pparams
+        jData['data-sheet'] = jDS
+
+        return jData
+
+    #------------------------------------------------------------------------
+    # For a single named member (memPath) out of an open tarfile (tarf),
+    # determine if it is a JSON file, and attempt to extract value of entry
+    # 'node' in dictionary entry 'data-sheet'.  Otherwise return None.
+    #------------------------------------------------------------------------
+
+    def tarMemberHasFoundryNode(self, tarf, memPath):
+        fileJSON = tarf.extractfile(memPath)
+        if not fileJSON: return None
+
+        try:
+            # NOTE: tarfile data is in bytes, json.load(fileJSON) does not work.
+            datatop = json.loads(fileJSON.read().decode('utf-8'))
+        except:
+            print("Failed to load extract file " + memPath + " as JSON data")
+            return None
+        else:
+            node = None
+            if 'data-sheet' in datatop:
+                dsheet = datatop['data-sheet']
+                if 'node' in dsheet:
+                    node = dsheet['node']
+
+        fileJSON.close()     # close open-tarfile before any return
+        return node
+
+    #------------------------------------------------------------------------
+    # For a single named member (memPath) out of an open tarfile (tarf),
+    # determine if first line embeds (case-insensitive match): Generated by Yosys
+    # Return True or False. If no such member or it has no 1st line, returns False.
+    #------------------------------------------------------------------------
+
+    def tarMemberIsGLverilog(self, tarf, memPath):
+        fileHdl = tarf.extractfile(memPath)
+        if not fileHdl: return False
+
+        line = fileHdl.readline()
+        fileHdl.close()     # close open-tarfile before any return
+        if not line: return False
+        return ('generated by yosys' in line.decode('utf-8').lower())
+
+    #------------------------------------------------------------------------
+    # Import vgl-netlist file INTO existing project.
+    # The importfile can be a .v; or a .json-with-tar that embeds a .v.
+    # What is newfile? not used here.
+    #
+    # PROMPT to select an existing project is here.
+    # (Is also a PROMPT to select existing electric lib, but that's within importvgl).
+    #------------------------------------------------------------------------
+
+    def importvglinto(self, newfile, importfile):
+        # Require existing project location
+        ppath = ExistingProjectDialog(self, self.get_project_list()).result
+        if not ppath:   return 0		# Canceled in dialog, no action.
+        pname = os.path.split(ppath)[1]
+        print( "Importing into existing project: %s" % (pname))
+
+        return self.importvgl(newfile, importfile, pname)
+
+    #------------------------------------------------------------------------
+    # Import cloudv project as new project.
+    #------------------------------------------------------------------------
+
+    def install_from_cloudv(self, opath, ppath, pdkname, stdcellname, ydicts):
+        oname = os.path.split(opath)[1]
+        pname = os.path.split(ppath)[1]
+
+        print('Cloudv project name is ' + str(oname))
+        print('New Open Galaxy project name is ' + str(pname))
+
+        os.makedirs(ppath + '/verilog', exist_ok=True)
+
+        vfile = None
+        isfullchip = False
+        ipname = oname
+
+        # First check for single synthesized projects, or all synthesized
+	# digital sub-blocks within a full-chip project.
+
+        os.makedirs(ppath + '/verilog/source', exist_ok=True)
+        bfiles = glob.glob(opath + '/build/*.netlist.v')
+        for bfile in bfiles:
+            tname = os.path.split(bfile)[1]
+            vname = os.path.splitext(os.path.splitext(tname)[0])[0]
+            tfile = ppath + '/verilog/' + vname + '/' + vname + '.vgl'
+            print('Making qflow sub-project ' + vname)
+            os.makedirs(ppath + '/verilog/' + vname, exist_ok=True)
+            shutil.copy(bfile, tfile)
+            if vname == oname:
+                vfile = tfile
+
+            # Each build project gets its own qflow directory.  Create the
+            # source/ subdirectory and make a link back to the .vgl file.
+            # qflow prep should do the rest.
+
+            os.makedirs(ppath + '/qflow', exist_ok=True)
+            os.makedirs(ppath + '/qflow/' + vname)
+            os.makedirs(ppath + '/qflow/' + vname + '/source')
+
+            # Make sure the symbolic link is relative, so that it is portable
+            # through a shared project.
+            curdir = os.getcwd()
+            os.chdir(ppath + '/qflow/' + vname + '/source')
+            os.symlink('../../../verilog/' + vname + '/' + vname + '.vgl', vname + '.v')
+            os.chdir(curdir)
+
+            # Create a simple qflow_vars.sh file so that the project manager
+            # qflow launcher will see it as a qflow sub-project.  If the meta.yaml
+            # file has a "stdcell" entry for the subproject, then add the line
+            # "techname=" with the name of the standard cell library as pulled
+            # from meta.yaml.
+
+            stdcell = None
+            buildname = 'build/' + vname + '.netlist.v'
+            for ydict in ydicts:
+                if buildname in ydict:
+                    yentry = ydict[buildname]
+                    if 'stdcell' in yentry:
+                        stdcell = yentry['stdcell']
+
+            with open(ppath + '/qflow/' + vname + '/qflow_vars.sh', 'w') as ofile:
+                print('#!/bin/tcsh -f', file=ofile)
+                if stdcell:
+                    print('set techname=' + stdcell, file=ofile)
+
+        # Now check for a full-chip verilog SoC (from CloudV)
+
+        modrex = re.compile('[ \t]*module[ \t]+[^ \t(]*_?soc[ \t]*\(')
+        genmodrex = re.compile('[ \t]*module[ \t]+([^ \t(]+)[ \t]*\(')
+
+        bfiles = glob.glob(opath + '/*.model/*.v')
+        for bfile in bfiles:
+            tname = os.path.split(bfile)[1]
+            vpath = os.path.split(bfile)[0]
+            ipname = os.path.splitext(tname)[0]
+            tfile = ppath + '/verilog/' + ipname + '.v'
+            isfullchip = True
+            break
+
+        if isfullchip:
+            print('Cloudv project IP name is ' + str(ipname))
+
+            # All files in */ paths should be copied to project verilog/source/,
+            # except for the module containing the SoC itself.  Note that the actual
+            # verilog source goes here, not the synthesized netlist, although that is
+            # mainly for efficiency of the simulation, which would normally be done in
+            # cloudV and not in Open Galaxy.  For Open Galaxy, what is needed is the
+            # existence of a verilog file containing a module name, which is used to
+            # track down the various files (LEF, DEF, etc.) that are needed for full-
+            # chip layout.
+            #
+            # (Sept. 2019) Added copying of files in /SW/ -> /sw/ and /Verify/ ->
+	    # /verify/ for running full-chip simulations on the Open Galaxy side.
+
+            os.makedirs(ppath + '/verilog', exist_ok=True)
+
+            cfiles = glob.glob(vpath + '/source/*')
+            for cfile in cfiles:
+                cname = os.path.split(cfile)[1]
+                if cname != tname:
+                    tpath = ppath + '/verilog/source/' + cname
+                    os.makedirs(ppath + '/verilog/source', exist_ok=True)
+                    shutil.copy(cfile, tpath)
+
+            cfiles = glob.glob(vpath + '/verify/*')
+            for cfile in cfiles:
+                cname = os.path.split(cfile)[1]
+                tpath = ppath + '/verilog/verify/' + cname
+                os.makedirs(ppath + '/verilog/verify', exist_ok=True)
+                shutil.copy(cfile, tpath)
+
+            cfiles = glob.glob(vpath + '/sw/*')
+            for cfile in cfiles:
+                cname = os.path.split(cfile)[1]
+                tpath = ppath + '/verilog/sw/' + cname
+                os.makedirs(ppath + '/verilog/sw', exist_ok=True)
+                shutil.copy(cfile, tpath)
+
+            # Read the top-level SoC verilog and recast it for OpenGalaxy.
+            with open(bfile, 'r') as ifile:
+                chiplines = ifile.read().splitlines()
+
+            # Find the modules used, track them down, and add the source location
+            # in the Open Galaxy environment as an "include" line in the top level
+            # verilog.
+
+            parentdir = os.path.split(bfile)[0]
+            modfile = parentdir + '/docs/modules.txt'
+
+            modules = []
+            if os.path.isfile(modfile):
+                with open(modfile, 'r') as ifile:
+                    modules = ifile.read().splitlines()
+            else:
+                print("Warning:  No modules.txt file for the chip top level module in "
+				+ parentdir + "/docs/.\n")
+
+            # Get the names of verilog libraries in this PDK.
+            pdkdir = os.path.realpath(ppath + '/.ef-config/techdir')
+            pdkvlog = pdkdir + '/libs.ref/verilog'
+            pdkvlogfiles = glob.glob(pdkvlog + '/*/*.v')
+
+            # Read the verilog libraries and create a dictionary mapping each
+            # module name to a location of the verilog file where it is located.
+            moddict = {}
+            for vlogfile in pdkvlogfiles:
+                with open(vlogfile, 'r') as ifile:
+                    for line in ifile.read().splitlines():
+                        mmatch = genmodrex.match(line)
+                        if mmatch:
+                            modname = mmatch.group(1)
+                            moddict[modname] = vlogfile
+
+            # Get the names of verilog libraries in the user IP space.
+            # (TO DO:  Need to know the IP version being used!)
+            designdir = os.path.split(ppath)[0]
+            ipdir = designdir + '/ip/'
+            uservlogfiles = glob.glob(ipdir + '/*/*/verilog/*.v')
+            for vlogfile in uservlogfiles:
+                # Strip ipdir from the front
+                vlogpath = vlogfile.replace(ipdir, '', 1)
+                with open(vlogfile, 'r') as ifile:
+                    for line in ifile.read().splitlines():
+                        mmatch = genmodrex.match(line)
+                        if mmatch:
+                            modname = mmatch.group(1)
+                            moddict[modname] = vlogpath
+
+            # Find all netlist builds from the project (those that were copied above)
+            buildfiles = glob.glob(ppath + '/verilog/source/*.v')
+            for vlogfile in buildfiles:
+                # Strip ipdir from the front
+                vlogpath = vlogfile.replace(ppath + '/verilog/source/', '', 1)
+                with open(vlogfile, 'r') as ifile:
+                    for line in ifile.read().splitlines():
+                        mmatch = genmodrex.match(line)
+                        if mmatch:
+                            modname = mmatch.group(1)
+                            moddict[modname] = vlogpath
+
+            # (NOTE:  removing 'ifndef LVS' as netgen should be able to handle
+            #  the contents of included files, and they are preferred since any
+            #  arrays are declared in each module I/O)
+            # chiplines.insert(0, '`endif')
+            chiplines.insert(0, '//--- End of list of included module dependencies ---')
+            includedfiles = []
+            for module in modules:
+                # Determine where this module comes from.  Look in the PDK, then in
+                # the user ip/ directory, then in the local hierarchy.  Note that
+                # the local hierarchy expects layouts from synthesized netlists that
+                # have not yet been created, so determine the expected location.
+
+                if module in moddict:
+                    if moddict[module] not in includedfiles:
+                        chiplines.insert(0, '`include "' + moddict[module] + '"')
+                        includedfiles.append(moddict[module])
+
+            # chiplines.insert(0, '`ifndef LVS')
+            chiplines.insert(0, '//--- List of included module dependencies ---')
+            chiplines.insert(0, '// iverilog simulation requires the use of -I source -I ~/design/ip')
+            chiplines.insert(0, '// NOTE:  Includes may be rooted at ~/design/ip/ or at ./source')
+            chiplines.insert(0, '// SoC top level verilog copied and modified by project manager')
+
+            # Copy file, but replace the module name "soc" with the ip-name
+            with open(tfile, 'w') as ofile:
+                for chipline in chiplines:
+                    print(modrex.sub('module ' + ipname + ' (', chipline), file=ofile)
+
+        # Need to define behavior:  What if there is more than one netlist?
+        # Which one is to be imported?  For now, ad-hoc behavior is to select
+        # the last netlist file in the list if no file matches the ip-name.
+
+        # Note that for full-chip projects, the full chip verilog file is always
+        # the last one set.
+
+        if not vfile:
+            try:
+                vfile = tfile
+            except:
+                pass
+
+        # NOTE:  vfile was being used to create a symbol, but not any more;
+        # see below.  All the above code referencing vfile can probably be
+        # removed.
+
+        try:
+            sfiles = glob.glob(vpath + '/source/*')
+            sfiles.extend(glob.glob(vpath + '/*/source/*'))
+        except:
+            sfiles = glob.glob(opath + '/*.v')
+            sfiles.extend(glob.glob(opath + '/*.sv'))
+            sfiles.extend(glob.glob(opath + '/local/*'))
+
+        for fname in sfiles:
+            sname = os.path.split(fname)[1]
+            tfile = ppath + '/verilog/source/' + sname
+            # Reject '.model' and '.soc" files (these are meaningful only to CloudV)
+            fileext = os.path.splitext(fname)[1]
+            if fileext == '.model' or fileext == '.soc':
+                continue
+            if os.path.isfile(fname):
+                # Check if /verilog/source/ has been created
+                if not os.path.isdir(ppath + '/verilog/source'):
+                    os.makedirs(ppath + '/verilog/source')
+                shutil.copy(fname, tfile)
+
+        # Add standard cell library name to project.json
+        pjsonfile = ppath + '/project.json'
+        if os.path.exists(pjsonfile):
+            with open(pjsonfile, 'r') as ifile:
+                datatop = json.load(ifile)
+        else:
+            datatop = self.create_ad_hoc_json(ipname, ppath)
+
+        # Generate a symbol in electric for the verilog top module
+        iconfile = ppath + '/elec/' + ipname + '.delib/' + ipname + '.ic'
+        if not os.path.exists(iconfile):
+            # NOTE:  Symbols are created by qflow migration for project
+            # builds.  Only the chip top-level needs to run create_symbol
+            # here.
+
+            if isfullchip:
+                print("Creating symbol for module " + ipname + " automatically from verilog source.")
+                create_symbol(ppath, vfile, ipname, iconfile, False)
+            # Add header file
+            self.create_electric_header_file(ppath, ipname)
+          
+        dsheet = datatop['data-sheet']
+        if not stdcellname or stdcellname == "":
+            dsheet['standard-cell'] = 'default'
+        else:
+            dsheet['standard-cell'] = stdcellname
+
+        with open(pjsonfile, 'w') as ofile:
+            json.dump(datatop, ofile, indent = 4)
+
+        return 0
+
+    #------------------------------------------------------------------------
+    # Import vgl-netlist AS new project.
+    # The importfile can be a .v; or a .json-with-tar that embeds a .v.
+    # What is newfile? not used here.
+    #
+    # PROMPT to select an create new project is within importvgl.
+    #------------------------------------------------------------------------
+
+    def importvglas(self, newfile, importfile, seedname):
+        print('importvglas:  seedname is ' + str(seedname))
+        return self.importvgl(newfile, importfile, newname=None, seedname=seedname)
+
+    #------------------------------------------------------------------------
+    # Utility shared/used by both: Import vgl-netlist file AS or INTO a project.
+    # Called directly for AS. Called via importvglinto for INTO.
+    #   importfile : source of .v to import, actual .v or json-with-tar that embeds a .v
+    #   newfile : not used
+    #   newname : target project-name (INTO), or None (AS: i.e. prompt to create one).
+    # Either newname is given: we PROMPT to pick an existing elecLib;
+    # Else PROMPT for new projectName and CREATE it (and use elecLib of same name).
+    #------------------------------------------------------------------------
+
+    def importvgl(self, newfile, importfile, newname=None, seedname=None):
+        elecLib = None
+        isnew = not newname
+
+        # Up front:  Determine if this import has a .json file associated
+        # with it.  If so, then parse the JSON data to find if there is a
+        # foundry and node set for the project.  If so, then the foundry
+        # node is not selectable at time of import.  Likewise, if "isnew"
+        # is false, then we need to check if there is a directory called
+        # "newname" and if it is set to the same foundry node.  If not,
+        # then the import must be rejected.
+
+        tarVfile, jName, importnode = self.jsonTarVglImportable(importfile)
+
+        if isnew:
+            print('importvgl:  seedname is ' + str(seedname))
+            # Use create project code first to generate a valid project space.
+            newname = self.createproject(None, seedname, importnode)
+            if not newname: return 0		# Canceled in dialog, no action.
+            print("Importing as new project " + newname + ".")
+            elecLib = newname
+
+        ppath = self.projectdir + '/' + newname
+        if not elecLib:
+            choices = self.get_elecLib_list(newname)
+            if not choices:
+                print( "Aborted: No existing electric libraries found to import into.")
+                return 0
+                
+            elecLib = ExistingElecLibDialog(self, choices).result
+            if not elecLib:
+                # Never a just-created project to delete here: We only PROMPT to pick elecLib in non-new case.
+                return 0		# Canceled in dialog, no action.
+            
+            # Isolate just electric lib name without extension. ../a/b.delib -> b
+            elecLib = os.path.splitext(os.path.split(elecLib)[-1])[0]
+            print("Importing to project: %s, elecLib: %s" % (newname, elecLib))
+
+        # Determine isolated *.v as importactual. May be importfile or tar-member (as temp-file).
+        importactual = importfile
+        if tarVfile:
+            importactual = self.jsonTarMember2tempfile(importfile, tarVfile)
+            print("importing json-with-tar's member: %s" % (tarVfile))
+
+        if not os.path.isfile(importactual):
+            # TODO: should this be a raise instead?
+            print('Error determining *.v to import')
+            return None
+
+        result = self.vgl_install(importactual, newname, elecLib, newfile, isnew=isnew)
+        if result == None:
+            print('Error during install')
+            return None
+        elif result == 0: 
+            # Canceled, so do not remove the import
+            return 0
+        else: 
+            # If jName is non-NULL then there is a JSON file in the tarball.  This is
+            # to be used as the project JSON file.  Contents of file coming from
+            # CloudV are correct as of 12/8/2017.
+            pname = os.path.expanduser('~/design/' + newname)
+            legacyjname = pname + '/' + newname + '.json'
+            # New behavior 12/2018:  Project JSON file always named 'project.json'
+            jname = pname + '/project.json'
+
+            # Do not overwrite an existing JSON file.  Overwriting is a problem for
+            # "import into", as the files go into an existing project, which would
+            # normally have its own JSON file.
+
+            if not os.path.exists(jname) and not os.path.exists(legacyjname):
+                try:
+                    tarJfile = os.path.split(tarVfile)[0] + '/' + jName + '.json'
+                    importjson = self.jsonTarMember2tempfile(importfile, tarJfile)
+                except:
+                    jData = self.create_ad_hoc_json(newname, pname)
+    
+                    with open(jname, 'w') as ofile:
+                        json.dump(jData, ofile, indent = 4)
+
+                else:
+                    # Copy the temporary file pulled from the tarball and
+                    # remove the temporary file.
+                    shutil.copy(importjson, jname)
+                    os.remove(importjson)
+
+            # For time-being, if a tar.gz & json: archive them in the target project, also as extracted.
+            # Remove original file from imports area (either .v; or .json plus tar)
+            # plus temp-file if extracted from the tar.
+            if importactual != importfile:
+                os.remove(importactual)
+                pname = self.projectdir + '/' + newname
+                importd = pname + '/' + archiveimportdir      # global: archiveimportdir
+                os.makedirs(importd, exist_ok=True)
+                # Dirnames to embed a VISIBLE date (UTC) of when populated.
+                # TODO: improve dir naming or better way to store & understand later when it was processed (a log?),
+                # without relying on file-system mtime.
+                archived = tempfile.mkdtemp( dir=importd, prefix='{:%Y-%m-%d.%H:%M:%S}-'.format(datetime.datetime.utcnow()))
+                tarname = self.json2targz(importfile)
+                if tarname:
+                    with tarfile.open(tarname, mode='r:gz') as archive:
+                        for member in archive:
+                            archive.extract(member, archived)
+                self.moveJsonPlus(importfile, archived)
+            else:
+                self.removeJsonPlus(importfile)
+            return 1    # Success
+
+    #------------------------------------------------------------------------
+    # Prepare multiline "warning" indicating which files to install already exist.
+    # TODO: ugly, don't use a simple confirmation dialogue: present a proper table.
+    #------------------------------------------------------------------------
+    def installsConfirmMarkOverwrite(self, module, files):
+        warning = [ "For import of module: %s," % module ]
+        anyExists = False
+        for i in files:
+            exists = os.path.isfile(os.path.expanduser(i))
+            if exists: anyExists = True
+            warning += [ (" * " if exists else "   ") + i ]
+        if anyExists:
+            titleSuffix = "\nCONFIRM installation of (*: OVERWRITE existing):"
+        else:
+            titleSuffix = "\nCONFIRM installation of:"
+        warning[0] += titleSuffix
+        return ConfirmInstallDialog(self,   "\n".join(warning)).result
+
+    def vgl_install(self, importfile, pname, elecLib, newfile, isnew=True):
+        #--------------------------------------------------------------------
+        # Convert the in .v to: spi, cdl, elec-icon, elec-text-view forms.
+        # TODO: Prompt to confirm final install of 5 files in dir-structure.
+        #
+        # newfile: argument is not used. What is it for?
+        # Target project AND electricLib MAY BE same (pname) or different.
+        # Rest of the filenames are determined by the module name in the source .v.
+        #--------------------------------------------------------------------
+
+        newproject = self.projectdir + '/' + pname
+        try:
+            p = subprocess.run(['/ef/apps/bin/vglImport', importfile, pname, elecLib],
+                               stdout=subprocess.PIPE, stderr=subprocess.PIPE,
+                               check=True, universal_newlines=True)
+        except subprocess.CalledProcessError as e:
+            if hasattr(e, 'stdout') and e.stdout: print(e.stdout)
+            if hasattr(e, 'stderr') and e.stderr: print(e.stderr)
+            print('Error running vglImport: ' + str(e))
+            if isnew == True: shutil.rmtree(newproject)
+            return None
+        else:
+            dataLines = p.stdout.splitlines()
+            if p.stderr:
+                # Print error messages to console
+                for i in p.stderr.splitlines(): print(i)
+            if not dataLines or len(dataLines) != 11:
+                print('Error: vglImport has no output, or wrong #outputs (%d vs 11)' % len(dataLines))
+                if isnew == True: shutil.rmtree(newproject)
+                return None
+            else:
+                module = dataLines[0]
+                confirm = self.installsConfirmMarkOverwrite(module, dataLines[2::2])
+                if not confirm:
+                    print("Cancelled")
+                    if isnew == True: shutil.rmtree(newproject)
+                    return 0
+                # print("Proceed")
+                clean = dataLines[1:]
+                nbr = len(dataLines)
+                ndx = 1
+                # trap I/O errors and clean-up if any
+                try:
+                    while ndx+1 < nbr:
+                        trg = os.path.expanduser(dataLines[ndx+1])
+                        os.makedirs(os.path.dirname(trg), exist_ok=True)
+                        shutil.move(dataLines[ndx], trg)
+                        ndx += 2
+                except IOError as e:
+                    print('Error copying files: ' + str(e))
+                    for i in clean:
+                        with contextlib.suppress(FileNotFoundError): os.remove(i)
+                    if isnew == True: shutil.rmtree(newproject)
+                    return 0
+                print( "For import of module %s installed: %s" % (module, " ".join(dataLines[2::2])))
+                return 1    # Success
+
+
+    #------------------------------------------------------------------------
+    # Callback function from "Import Into" button on imports list box.
+    #------------------------------------------------------------------------
+
+    def importintodesign(self, value):
+        if not value['values']:
+            print('No import selected.')
+            return
+
+        # Stop the watchdog timer while this is going on
+        self.watchclock.stop()
+        newname = value['text']
+        importfile = value['values'][0]
+        print('Import project name: ' + newname + '')
+        print('Import file name: ' + importfile + '')
+
+        # Behavior depends on what kind of file is being imported.
+        # Tarballs are entire projects.  Other files are individual
+        # files and may be imported into new or existing projects
+
+        if os.path.isdir(importfile):
+            print('File is a project, must import as new project.')
+            result = self.import2project(importfile, addWarn='Redirected: A projectDir must Import-As new project.')
+        else:
+            ext = os.path.splitext(importfile)[1]
+            vFile, jName, importnode = self.jsonTarVglImportable(importfile)
+            if ((ext == '.json' and vFile) or ext == '.v'):
+                result = self.importvglinto(newname, importfile)
+            elif ext == '.json':
+                # Same behavior as "Import As", at least for now
+                print('File is a project, must import as new project.')
+                result = self.importjson(newname, importfile)
+            else:
+                result = self.importspiceinto(newname, importfile)
+
+        if result:
+            self.update_project_views(force=True)
+        self.watchclock.restart()
+
+    #------------------------------------------------------------------------
+    # Callback function from "Import As" button on imports list box.
+    #------------------------------------------------------------------------
+
+    def importdesign(self, value):
+        if not value['values']:
+            print('No import selected.')
+            return
+
+        # Stop the watchdog timer while this is going on
+        self.watchclock.stop()
+        newname = value['text']
+        importfile = value['values'][0]
+        print('Import project name: ' + newname)
+        print('Import file name: ' + importfile)
+
+        # Behavior depends on what kind of file is being imported.
+        # Tarballs are entire projects.  Other files are individual
+        # files and may be imported into new or existing projects
+
+        if os.path.isdir(importfile):
+            result = self.import2project(importfile)
+        else:
+            pathext = os.path.splitext(importfile)
+            vfile, seedname, importnode = self.jsonTarVglImportable(importfile)
+            if ((pathext[1] == '.json' and seedname) or pathext[1] == '.v'):
+                result = self.importvglas(newname, importfile, seedname)
+            elif pathext[1] == '.json':
+                result = self.importjson(newname, importfile)
+            else:
+                result = self.importspice(newname, importfile)
+
+        if result:
+            self.update_project_views(force=True)
+        self.watchclock.restart()
+
+    def deleteimport(self, value):
+        if not value['values']:
+            print('No import selected.')
+            return
+
+        print("Delete import " + value['text'] + '  ' + value['values'][0] + " !")
+        # Require confirmation
+        warning = 'Confirm delete import ' + value['text'] + '?'
+        confirm = ProtectedConfirmDialog(self, warning).result
+        if not confirm == 'okay':
+            return
+        print('Delete confirmed!')
+        item = value['values'][0]
+
+        if not os.path.islink(item) and os.path.isdir(item):
+            shutil.rmtree(item)
+            return
+
+        os.remove(item)
+        ext = os.path.splitext(item)
+        # Where import is a pair of .json and .tar.gz files, remove both.
+        if ext[1] == '.json':
+            if os.path.exists(ext[0] + '.tar.gz'):
+                os.remove(ext[0] + '.tar.gz')
+            elif os.path.exists(ext[0] + '.tgz'):
+                os.remove(ext[0] + '.tgz')
+
+    def update_project_views(self, force=False):
+        # More than updating project views, this updates projects, imports, and
+        # IP libraries.
+
+        projectlist = self.get_project_list()
+        self.projectselect.repopulate(projectlist)
+        pdklist = self.get_pdk_list(projectlist)
+        self.projectselect.populate2("PDK", projectlist, pdklist)
+
+        old_imports = self.number_of_imports
+        importlist = self.get_import_list()
+        self.importselect.repopulate(importlist)
+        valuelist = self.importselect.getvaluelist()
+        datelist = self.get_date_list(valuelist)
+        itemlist = self.importselect.getlist()
+        self.importselect.populate2("date", itemlist, datelist)
+
+        # To do:  Check if itemlist in imports changed, and open if a new import
+        # has arrived.
+     
+        if force or (old_imports != None) and (old_imports < self.number_of_imports):
+            self.import_open()
+
+        iplist = self.get_library_list()
+        self.ipselect.repopulate(iplist, versioning=True)
+        valuelist = self.ipselect.getvaluelist()
+        datelist = self.get_date_list(valuelist)
+        itemlist = self.ipselect.getlist()
+        self.ipselect.populate2("date", itemlist, datelist)
+        
+    def update_alert(self):
+        # Project manager has been updated.  Generate an alert window and
+        # provide option to restart the project manager.
+
+        warning = 'Project manager app has been updated.  Restart now?'
+        confirm = ConfirmDialog(self, warning).result
+        if not confirm == 'okay':
+            print('Warning: Must quit and restart to get any fixes or updates.')
+            return
+        os.execl('/ef/efabless/opengalaxy/og_gui_manager.py', 'appsel_zenity.sh')
+        # Does not return; replaces existing process.
+
+    #----------------------------------------------------------------------
+    # Delete a project from the design folder.
+    #----------------------------------------------------------------------
+
+    def deleteproject(self, value):
+        if not value['values']:
+            print('No project selected.')
+            return
+        print('Delete project ' + value['values'][0])
+        # Require confirmation
+        warning = 'Confirm delete entire project ' + value['text'] + '?'
+        confirm = ProtectedConfirmDialog(self, warning).result
+        if not confirm == 'okay':
+            return
+        shutil.rmtree(value['values'][0])
+
+    #----------------------------------------------------------------------
+    # Clean out the simulation folder.  Traditionally this was named
+    # 'ngspice', so this is checked for backward-compatibility.  The
+    # proper name of the simulation directory is 'simulation'.
+    #----------------------------------------------------------------------
+
+    def cleanproject(self, value):
+        if not value['values']:
+            print('No project selected.')
+            return
+        ppath = value['values'][0]
+        print('Clean simulation raw data from directory ' + ppath)
+        # Require confirmation
+        warning = 'Confirm clean project ' + value['text'] + ' contents?'
+        confirm = ConfirmDialog(self, warning).result
+        if not confirm == 'okay':
+            return
+        if os.path.isdir(ppath + '/simulation'):
+            simpath = 'simulation'
+        elif os.path.isdir(ppath + '/ngspice'):
+            simpath = 'ngspice'
+        else:
+            print('Project has no simulation folder.')
+            return
+
+        filelist = os.listdir(ppath + '/' + simpath)
+        for sfile in filelist:
+            if os.path.splitext(sfile)[1] == '.raw':
+                os.remove(ppath + '/ngspice/' + sfile)                
+        print('Project simulation folder cleaned.')
+
+        # Also clean the log file
+        filelist = os.listdir(ppath)
+        for sfile in filelist:
+            if os.path.splitext(sfile)[1] == '.log':
+                os.remove(ppath + '/' + sfile)                
+
+    #---------------------------------------------------------------------------------------
+    # Determine which schematic editors are compatible with the PDK, and return a list of them.
+    #---------------------------------------------------------------------------------------
+
+    def list_valid_schematic_editors(self, pdktechdir):
+        # Check PDK technology directory for xcircuit, xschem, and electric
+        applist = []
+        if os.path.exists(pdktechdir + '/elec'):
+            applist.append('electric')
+        if os.path.exists(pdktechdir + '/xschem'):
+            applist.append('xschem')
+        if os.path.exists(pdktechdir + '/xcircuit'):
+            applist.append('xcircuit')
+
+        return applist
+
+    #------------------------------------------------------------------------------------------
+    # Determine which layout editors are compatible with the PDK, and return a list of them.
+    #------------------------------------------------------------------------------------------
+
+    def list_valid_layout_editors(self, pdktechdir):
+        # Check PDK technology directory for magic and klayout
+        applist = []
+        if os.path.exists(pdktechdir + '/magic'):
+            applist.append('magic')
+        if os.path.exists(pdktechdir + '/klayout'):
+            applist.append('klayout')
+        return applist
+
+    #----------------------------------------------------------------------
+    # Create a new project folder and initialize it (see below for steps)
+    #----------------------------------------------------------------------
+
+    def createproject(self, value, seedname=None, importnode=None):
+        # Note:  value is current selection, if any, and is ignored
+        # Require new project location and confirmation
+        badrex1 = re.compile("^\.")
+        badrex2 = re.compile(".*[/ \t\n\\\><\*\?].*")
+        warning = 'Create new project:'
+        print(warning)
+        development = self.prefs['development']
+        while True:
+            try:
+                if seedname:
+                    newname, newpdk = NewProjectDialog(self, warning, seed=seedname, importnode=importnode, development=development).result
+                else:
+                    newname, newpdk = NewProjectDialog(self, warning, seed='', importnode=importnode, development=development).result
+            except TypeError:
+                # TypeError occurs when "Cancel" is pressed, just handle exception.
+                return None
+            if not newname:
+                return None	# Canceled, no action.
+
+            newproject = self.projectdir + '/' + newname
+            if self.blacklisted(newname):
+                warning = newname + ' is not allowed for a project name.'
+            elif badrex1.match(newname):
+                warning = 'project name may not start with "."'
+            elif badrex2.match(newname):
+                warning = 'project name contains illegal characters or whitespace.'
+            elif os.path.exists(newproject):
+                warning = newname + ' is already a project name.'
+            else:
+                break
+        
+        try:
+            
+            subprocess.Popen([og_config.apps_path + '/create_project.py', newproject, newpdk])
+            
+        except IOError as e:
+            print('Error copying files: ' + str(e))
+            return None
+            
+        except:
+            print('Error making project.')
+            return None
+            
+        return newname
+        '''
+        # Find what tools are compatible with the given PDK
+        schemapps = self.list_valid_schematic_editors(newpdk + '/libs.tech')
+        layoutapps = self.list_valid_layout_editors(newpdk + '/libs.tech')
+
+        print('New project name will be ' + newname + '.')
+        print('Associated project PDK is ' + newpdk + '.')
+        try:
+            os.makedirs(newproject)
+
+            # Make standard folders
+            if 'magic' in layoutapps:
+                os.makedirs(newproject + '/mag')
+
+            os.makedirs(newproject + '/spi')
+            os.makedirs(newproject + '/spi/pex')
+            os.makedirs(newproject + '/spi/lvs')
+            if 'electric' in layoutapps or 'electric' in schemapps:
+                os.makedirs(newproject + '/elec')
+            if 'xcircuit' in schemapps:
+                os.makedirs(newproject + '/xcirc')
+            if 'klayout' in schemapps:
+                os.makedirs(newproject + '/klayout')
+            os.makedirs(newproject + '/ngspice')
+            os.makedirs(newproject + '/ngspice/run')
+            if 'electric' in schemapps:
+                os.makedirs(newproject + '/ngspice/run/.allwaves')
+            os.makedirs(newproject + '/testbench')
+            os.makedirs(newproject + '/verilog')
+            os.makedirs(newproject + '/verilog/source')
+            os.makedirs(newproject + '/.ef-config')
+            if 'xschem' in schemapps:
+                os.makedirs(newproject + '/xschem')
+
+            pdkname = os.path.split(newpdk)[1]
+
+            # Symbolic links
+            os.symlink(newpdk, newproject + '/.ef-config/techdir')
+
+            # Copy preferences
+            # deskel = '/ef/efabless/deskel'
+            #
+            # Copy examples (disabled;  this is too confusing to the end user.  Also, they
+            # should not be in user space at all, as they are not user editable.
+            #
+            # for item in os.listdir(deskel + '/exlibs'):
+            #     shutil.copytree(deskel + '/exlibs/' + item, newproject + '/elec/' + item)
+            # for item in os.listdir(deskel + '/exmag'):
+            #     if os.path.splitext(item)[1] == '.mag':
+            #         shutil.copy(deskel + '/exmag/' + item, newproject + '/mag/' + item)
+
+            # Put tool-specific startup files into the appropriate user directories.
+            if 'electric' in layoutapps or 'electric' in schemapps:
+                self.reinitElec(newproject)   # [re]install elec/.java, elec/LIBDIRS if needed, from pdk-specific if-any
+                # Set up electric
+                self.create_electric_header_file(newproject, newname)
+
+            if 'magic' in layoutapps:
+                shutil.copy(newpdk + '/libs.tech/magic/current/' + pdkname + '.magicrc', newproject + '/mag/.magicrc')
+
+            if 'xcircuit' in schemapps:
+                xcircrc = newpdk + '/libs.tech/xcircuit/' + pdkname + '.' + 'xcircuitrc'
+                xcircrc2 = newpdk + '/libs.tech/xcircuit/xcircuitrc'
+                if os.path.exists(xcircrc):
+                    shutil.copy(xcircrc, newproject + '/xcirc/.xcircuitrc')
+                elif os.path.exists(xcircrc2):
+                    shutil.copy(xcircrc2, newproject + '/xcirc/.xcircuitrc')
+
+            if 'xschem' in schemapps:
+                xschemrc = newpdk + '/libs.tech/xschem/xschemrc'
+                if os.path.exists(xschemrc):
+                    shutil.copy(xschemrc, newproject + '/xschem/xschemrc')
+
+        except IOError as e:
+            print('Error copying files: ' + str(e))
+            return None
+
+        return newname
+        '''
+    #----------------------------------------------------------------------
+    # Import a CloudV project from ~/cloudv/<project_name>
+    #----------------------------------------------------------------------
+
+    def cloudvimport(self, value):
+
+        # Require existing project location
+        clist = self.get_cloudv_project_list()
+        if not clist:
+            return 0		# No projects to import
+        ppath = ExistingProjectDialog(self, clist, warning="Enter name of cloudV project to import:").result
+        if not ppath:
+            return 0		# Canceled in dialog, no action.
+        pname = os.path.split(ppath)[1]
+        print("Importing CloudV project " + pname)
+
+        importnode = None
+        stdcell = None
+        netlistfile = None
+
+        # Pull process and standard cell library from the YAML file created by
+        # CloudV.  NOTE:  YAML file has multiple documents, so must use
+        # yaml.load_all(), not yaml.load().  If there are refinements of this
+        # process for individual build files, they will override (see further down).
+
+        # To do:  Check entries for SoC builds.  If there are multiple SoC builds,
+        # then create an additional drop-down selection to choose one, since only
+        # one SoC build can exist as a single Open Galaxy project.  Get the name
+        # of the top-level module for the SoC.  (NOTE:  It may not be intended
+        # that there can be multiple SoC builds in the project, so for now retaining
+        # the existing parsing assuming default names.)
+
+        if os.path.exists(ppath + '/.ef-config/meta.yaml'):
+            print("Reading YAML file:")
+            ydicts = []
+            with open(ppath + '/.ef-config/meta.yaml', 'r') as ifile:
+                yalldata = yaml.load_all(ifile, Loader=yaml.Loader)
+                for ydict in yalldata:
+                    ydicts.append(ydict)
+
+            for ydict in ydicts:
+                for yentry in ydict.values():
+                    if 'process' in yentry:
+                        importnode = yentry['process']
+
+        # If there is a file ().soc and a directory ().model, then pull the file
+        # ().model/().model.v, which is a chip top-level netlist.
+
+        ydicts = []
+        has_soc = False
+        save_vdir = None
+        vdirs = glob.glob(ppath + '/*')
+        for vdir in vdirs:
+            vnameparts = vdir.split('.')
+            if len(vnameparts) > 1 and vnameparts[-1] == 'soc' and os.path.isdir(vdir):
+                has_soc = True
+            if len(vnameparts) > 1 and vnameparts[-1] == 'model':
+                save_vdir = vdir
+
+        if has_soc:
+            if save_vdir:
+                vdir = save_vdir
+                print("INFO:  CloudV project " + vdir + " is a full chip SoC project.")
+
+                vroot = os.path.split(vdir)[1]
+                netlistfile = vdir + '/' + vroot + '.v'
+                if os.path.exists(netlistfile):
+                    print("INFO:  CloudV chip top level verilog is " + netlistfile + ".")
+            else:
+                print("ERROR:  Expected SoC .model directory not found.")
+
+        # Otherwise, if the project has a build/ directory and a netlist.v file,
+        # then set the foundry node accordingly.
+
+        elif os.path.exists(ppath + '/build'):
+            vfiles = glob.glob(ppath + '/build/*.v')
+            for vfile in vfiles:
+                vroot = os.path.splitext(vfile)[0]
+                if os.path.splitext(vroot)[1] == '.netlist':
+                    netlistfile = ppath + '/build/' + vfile
+
+                    # Pull process and standard cell library from the YAML file
+                    # created by CloudV
+                    # Use yaml.load_all(), not yaml.load() (see above)
+
+                    if os.path.exists(ppath + '/.ef-config/meta.yaml'):
+                        print("Reading YAML file:")
+                        ydicts = []
+                        with open(ppath + '/.ef-config/meta.yaml', 'r') as ifile:
+                            yalldata = yaml.load_all(ifile, Loader=yaml.Loader)
+                            for ydict in yalldata:
+                                ydicts.append(ydict)
+
+                        for ydict in ydicts:
+                            for yentry in ydict.values():
+                                if 'process' in yentry:
+                                    importnode = yentry['process']
+                                if 'stdcell' in yentry:
+                                    stdcell = yentry['stdcell']
+                    break
+
+        if importnode:
+            print("INFO:  Project targets foundry process " + importnode + ".")
+        else:
+            print("WARNING:  Project does not target any foundry process.")
+
+        newname = self.createproject(value, seedname=pname, importnode=importnode)
+        if not newname: return 0		# Canceled in dialog, no action.
+        newpath = self.projectdir + '/' + newname
+
+        result = self.install_from_cloudv(ppath, newpath, importnode, stdcell, ydicts)
+        if result == None:
+            print('Error during import.')
+            return None
+        elif result == 0:
+            return 0    # Canceled
+        else:
+            return 1    # Success
+
+    #----------------------------------------------------------------------
+    # Make a copy of a project in the design folder.
+    #----------------------------------------------------------------------
+
+    def copyproject(self, value):
+        if not value['values']:
+            print('No project selected.')
+            return
+        # Require copy-to location and confirmation
+        badrex1 = re.compile("^\.")
+        badrex2 = re.compile(".*[/ \t\n\\\><\*\?].*")
+        warning = 'Copy project ' + value['text'] + ' to new project.'
+        print('Copy project directory ' + value['values'][0])
+        newname = ''
+        copylist = []
+        elprefs = False
+        spprefs = False
+        while True:
+            copylist = CopyProjectDialog(self, warning, seed=newname).result
+            if not copylist:
+                return		# Canceled, no action.
+            else:
+                newname = copylist[0]
+                elprefs = copylist[1]
+                spprefs = copylist[2]
+            newproject = self.projectdir + '/' + newname
+            if self.blacklisted(newname):
+                warning = newname + ' is not allowed for a project name.'
+            elif badrex1.match(newname):
+                warning = 'project name may not start with "."'
+            elif badrex2.match(newname):
+                warning = 'project name contains illegal characters or whitespace.'
+            elif os.path.exists(newproject):
+                warning = newname + ' is already a project name.'
+            else:
+                break
+
+        oldpath = value['values'][0]
+        oldname = os.path.split(oldpath)[1]
+        patterns = [oldname + '.log']
+        if not elprefs:
+            patterns.append('.java')
+        if not spprefs:
+            patterns.append('ngspice')
+            patterns.append('pv')
+
+        print("New project name will be " + newname)
+        try:
+            shutil.copytree(oldpath, newproject, symlinks = True,
+			ignore = shutil.ignore_patterns(*patterns))
+        except IOError as e:
+            print('Error copying files: ' + str(e))
+            return
+
+        # NOTE:  Behavior is for project files to depend on "ip-name".  Using
+        # the project filename as a project name is a fallback behavior.  If
+        # there is a project.json file, and it defines an ip-name entry, then
+        # there is no need to make changes within the project.  If there is
+        # no project.json file, then create one and set the ip-name entry to
+        # the old project name, which avoids the need to make changes within
+        # the project.
+
+        else:
+            # Check project.json
+            jsonname = newproject + '/project.json'
+            legacyname = newproject + '/' + oldname + '.json'
+            if not os.path.isfile(jsonname):
+                if os.path.isfile(legacyname):
+                    jsonname = legacyname
+
+            found = False
+            if os.path.isfile(jsonname):
+                # Pull the ipname into local store (may want to do this with the
+                # datasheet as well)
+                with open(jsonname, 'r') as f:
+                    datatop = json.load(f)
+                    dsheet = datatop['data-sheet']
+                    if 'ip-name' in dsheet:
+                        found = True
+
+            if not found:
+                jData = self.create_ad_hoc_json(oldname, newproject)
+                with open(newproject + '/project.json', 'w') as ofile:
+                    json.dump(jData, ofile, indent = 4)
+
+        # If ngspice and electric prefs were not copied from the source
+        # to the target, as recommended, then copy these from the
+        # skeleton repository as is done when creating a new project.
+
+        if not spprefs:
+            try:
+                os.makedirs(newproject + '/ngspice')
+                os.makedirs(newproject + '/ngspice/run')
+                os.makedirs(newproject + '/ngspice/run/.allwaves')
+            except FileExistsError:
+                pass
+        if not elprefs:
+            # Copy preferences
+            deskel = '/ef/efabless/deskel'
+            try:
+                shutil.copytree(deskel + '/dotjava', newproject + '/elec/.java', symlinks = True)
+            except IOError as e:
+                print('Error copying files: ' + e)
+
+    #----------------------------------------------------------------------
+    # Change a project IP to a different name.
+    #----------------------------------------------------------------------
+
+    def renameproject(self, value):
+        if not value['values']:
+            print('No project selected.')
+            return
+
+        # Require new project name and confirmation
+        badrex1 = re.compile("^\.")
+        badrex2 = re.compile(".*[/ \t\n\\\><\*\?].*")
+        projname = value['text']
+
+        # Find the IP name for project projname.  If it has a JSON file, then
+        # read it and pull the ip-name record.  If not, the fallback position
+        # is to assume that the project filename is the project name.
+
+        # Check project.json
+        projectpath = self.projectdir + '/' + projname
+        jsonname = projectpath + '/project.json'
+        legacyname = projectpath + '/' + projname + '.json'
+        if not os.path.isfile(jsonname):
+            if os.path.isfile(legacyname):
+                jsonname = legacyname
+
+        oldname = projname
+        if os.path.isfile(jsonname):
+            # Pull the ipname into local store (may want to do this with the
+            # datasheet as well)
+            with open(jsonname, 'r') as f:
+                datatop = json.load(f)
+                dsheet = datatop['data-sheet']
+                if 'ip-name' in dsheet:
+                    oldname = dsheet['ip-name']
+
+        warning = 'Rename IP "' + oldname + '" for project ' + projname + ':'
+        print(warning)
+        newname = projname
+        while True:
+            try:
+                newname = ProjectNameDialog(self, warning, seed=oldname + '_1').result
+            except TypeError:
+                # TypeError occurs when "Cancel" is pressed, just handle exception.
+                return None
+            if not newname:
+                return None	# Canceled, no action.
+
+            if self.blacklisted(newname):
+                warning = newname + ' is not allowed for an IP name.'
+            elif badrex1.match(newname):
+                warning = 'IP name may not start with "."'
+            elif badrex2.match(newname):
+                warning = 'IP name contains illegal characters or whitespace.'
+            else:
+                break
+
+        # Update everything, including schematic, symbol, layout, JSON file, etc.
+        print('New project IP name will be ' + newname + '.')
+        rename_project_all(projectpath, newname)
+
+    # class vars: one-time compile of regulare expressions for life of the process
+    projNameBadrex1 = re.compile("^[-.]")
+    projNameBadrex2 = re.compile(".*[][{}()!/ \t\n\\\><#$\*\?\"'|`~]")
+    importProjNameBadrex1 = re.compile(".*[.]bak$")
+
+    # centralize legal projectName check.
+    # TODO: Several code sections are not yet converted to use this.
+    # TODO: Extend to explain to the user the reason why.
+    def validProjectName(self, name):
+        return not (self.blacklisted(name) or
+                    self.projNameBadrex1.match(name) or
+                    self.projNameBadrex2.match(name))
+
+    #----------------------------------------------------------------------
+    # "Import As" a dir in import/ as a project. based on renameproject().
+    # addWarn is used to augment confirm-dialogue if redirected here via erroneous ImportInto
+    #----------------------------------------------------------------------
+
+    def import2project(self, importfile, addWarn=None):
+        name = os.path.split(importfile)[1]
+        projpath = self.projectdir + '/' + name
+
+        bakname = name + '.bak'
+        bakpath = self.projectdir + '/' + bakname
+        warns = []
+        if addWarn:
+            warns += [ addWarn ]
+
+        # Require new project name and confirmation
+        confirmPrompt = None    # use default: I am sure I want to do this.
+        if os.path.isdir(projpath):
+            if warns:
+                warns += [ '' ]  # blank line between addWarn and below two Warnings:
+            if os.path.isdir(bakpath):
+                warns += [ 'Warning: Replacing EXISTING: ' + name + ' AND ' + bakname + '!' ]
+            else:
+                warns += [ 'Warning: Replacing EXISTING: ' + name + '!' ]
+            warns += [ 'Warning: Check for & exit any Electric,magic,qflow... for above project(s)!\n' ]
+            confirmPrompt = 'I checked & exited apps and am sure I want to do this.'
+
+        warns += [ 'Confirm import-as new project: ' + name + '?' ]
+        warning = '\n'.join(warns)
+        confirm = ProtectedConfirmDialog(self, warning, confirmPrompt=confirmPrompt).result
+        if not confirm == 'okay':
+            return
+
+        print('New project name will be ' + name + '.')
+        try:
+            if os.path.isdir(projpath):
+                if os.path.isdir(bakpath):
+                    print('Deleting old project: ' + bakpath);
+                    shutil.rmtree(bakpath)
+                print('Moving old project ' + name + ' to ' + bakname)
+                os.rename(                projpath,           bakpath)
+            print("Importing as new project " + name)
+            os.rename(importfile, projpath)
+            return True
+        except IOError as e:
+            print("Error importing-as project: " + str(e))
+            return None
+
+    #----------------------------------------------------------------------
+    # Helper subroutine:
+    # Check if a project is a valid project.  Return the name of the
+    # datasheet if the project has a valid one in the project top level
+    # path.
+    #----------------------------------------------------------------------
+
+    def get_datasheet_name(self, dpath):
+        if not os.path.isdir(dpath):
+            print('Error:  Project is not a folder!')
+            return
+        # Check for valid datasheet name in the following order:
+        # (1) project.json (Legacy)
+        # (2) <name of directory>.json (Legacy)
+        # (3) not "datasheet.json" or "datasheet_anno.json" 
+        # (4) "datasheet.json"
+        # (5) "datasheet_anno.json"
+
+        dsname = os.path.split(dpath)[1]
+        if os.path.isfile(dpath + '/project.json'):
+            datasheet = dpath + '/project.json'
+        elif os.path.isfile(dpath + '/' + dsname + '.json'):
+            datasheet = dpath + '/' + dsname + '.json'
+        else:
+            has_generic = False
+            has_generic_anno = False
+            filelist = os.listdir(dpath)
+            for file in filelist[:]:
+                if os.path.splitext(file)[1] != '.json':
+                    filelist.remove(file)
+            if 'datasheet.json' in filelist:
+                has_generic = True
+                filelist.remove('datasheet.json')
+            if 'datasheet_anno.json' in filelist:
+                has_generic_anno = True
+                filelist.remove('datasheet_anno.json')
+            if len(filelist) == 1:
+                print('Trying ' + dpath + '/' + filelist[0])
+                datasheet = dpath + '/' + filelist[0]
+            elif has_generic:
+                datasheet + dpath + '/datasheet.json'
+            elif has_generic_anno:
+                datasheet + dpath + '/datasheet_anno.json'
+            else:
+                if len(filelist) > 1:
+                    print('Error:  Path ' + dpath + ' has ' + str(len(filelist)) +
+                            ' valid datasheets.')
+                else:
+                    print('Error:  Path ' + dpath + ' has no valid datasheets.')
+                return None
+
+        if not os.path.isfile(datasheet):
+            print('Error:  File ' + datasheet + ' not found.')
+            return None
+        else:
+            return datasheet
+
+    #----------------------------------------------------------------------
+    # Run the LVS manager
+    #----------------------------------------------------------------------
+
+    def run_lvs(self):
+        value = self.projectselect.selected()
+        if value:
+            design = value['values'][0]
+            # designname = value['text']
+            designname = self.project_name
+            print('Run LVS on design ' + designname + ' (' + design + ')')
+            # use Popen, not run, so that application does not wait for it to exit.
+            subprocess.Popen([og_config.apps_path + '/lvs_manager.py', design, designname])
+        else:
+            print("You must first select a project.", file=sys.stderr)
+
+    #----------------------------------------------------------------------
+    # Run the local characterization checker
+    #----------------------------------------------------------------------
+
+    def characterize(self):
+        value = self.projectselect.selected()
+        if value:
+            design = value['values'][0]
+            # designname = value['text']
+            designname = self.project_name
+            datasheet = self.get_datasheet_name(design)
+            print('Characterize design ' + designname + ' (' + datasheet + ' )')
+            if datasheet:
+                # use Popen, not run, so that application does not wait for it to exit.
+                dsheetroot = os.path.splitext(datasheet)[0]
+                subprocess.Popen([og_config.apps_path + '/og_gui_characterize.py',
+				datasheet])
+        else:
+            print("You must first select a project.", file=sys.stderr)
+
+    #----------------------------------------------------------------------
+    # Run the local synthesis tool (qflow)
+    #----------------------------------------------------------------------
+
+    def synthesize(self):
+        value = self.projectselect.selected()
+        if value:
+            design = value['values'][0]
+            # designname = value['text']
+            designname = self.project_name
+            development = self.prefs['devstdcells']
+            if not designname:
+                # A project without a datasheet has no designname (which comes from
+                # the 'ip-name' record in the datasheet JSON) but can still be
+                # synthesized.
+                designname = design
+
+            # Normally there is one digital design in a project.  However, full-chip
+            # designs (in particular) may have multiple sub-projects that are
+            # independently synthesized digital blocks.  Find all subdirectories of
+            # the top level or subdirectories of qflow that contain a 'qflow_vars.sh'
+            # file.  If there is more than one, then present a list.  If there is
+            # only one but it is not in 'qflow/', then be sure to pass the actual
+            # directory name to the qflow manager.
+
+            qvlist = glob.glob(design + '/*/qflow_vars.sh')
+            qvlist.extend(glob.glob(design + '/qflow/*/qflow_vars.sh'))
+            if len(qvlist) > 1 or (len(qvlist) == 1 and not os.path.exists(design + '/qflow/qflow_vars.sh')):
+                # Generate selection menu
+                if len(qvlist) > 1:
+                    clist = list(os.path.split(item)[0] for item in qvlist)
+                    ppath = ExistingProjectDialog(self, clist, warning="Enter name of qflow project to open:").result
+                    if not ppath:
+                        return 0		# Canceled in dialog, no action.
+                else:
+                    ppath = os.path.split(qvlist[0])[0]
+
+                # pname is everything in ppath after matching design:
+                pname = ppath.replace(design + '/', '')
+
+                print('Synthesize design in qflow project directory ' + pname)
+                if development:
+                    subprocess.Popen([og_config.apps_path + '/qflow_manager.py',
+				design, '-development', '-subproject=' + pname])
+                else:
+                    subprocess.Popen([og_config.apps_path + '/qflow_manager.py',
+				design, '-subproject=' + pname])
+            else:
+                print('Synthesize design ' + designname + ' (' + design + ')')
+                # use Popen, not run, so that application does not wait for it to exit.
+                if development:
+                    subprocess.Popen([og_config.apps_path + '/qflow_manager.py',
+				design, designname, '-development'])
+                else:
+                    subprocess.Popen([og_config.apps_path + '/qflow_manager.py',
+				design, designname])
+        else:
+            print("You must first select a project.", file=sys.stderr)
+
+    #----------------------------------------------------------------------
+    # Switch between showing and hiding the import list (default hidden)
+    #----------------------------------------------------------------------
+
+    def import_toggle(self):
+        import_state = self.toppane.import_frame.import_header3.cget('text')
+        if import_state == '+':
+            self.importselect.grid(row = 11, sticky = 'news')
+            self.toppane.import_frame.import_header3.config(text='-')
+        else:
+            self.importselect.grid_forget()
+            self.toppane.import_frame.import_header3.config(text='+')
+
+    def import_open(self):
+        self.importselect.grid(row = 11, sticky = 'news')
+        self.toppane.import_frame.import_header3.config(text='-')
+
+    #----------------------------------------------------------------------
+    # Switch between showing and hiding the IP library list (default hidden)
+    #----------------------------------------------------------------------
+
+    def library_toggle(self):
+        library_state = self.toppane.library_frame.library_header3.cget('text')
+        if library_state == '+':
+            self.ipselect.grid(row = 8, sticky = 'news')
+            self.toppane.library_frame.library_header3.config(text='-')
+        else:
+            self.ipselect.grid_forget()
+            self.toppane.library_frame.library_header3.config(text='+')
+
+    def library_open(self):
+        self.ipselect.grid(row = 8, sticky = 'news')
+        self.toppane.library_frame.library_header3.config(text='-')
+
+    #----------------------------------------------------------------------
+    # Run padframe-calc (today internally invokes libreoffice, we only need cwd set to design project)
+    #----------------------------------------------------------------------
+    def padframe_calc(self):
+        value = self.projectselect.selected()
+        if value:
+            designname = self.project_name
+            self.padframe_calc_work(newname=designname)
+        else:
+            print("You must first select a project.", file=sys.stderr)
+
+    #------------------------------------------------------------------------
+    # Run padframe-calc (today internally invokes libreoffice, we set cwd to design project)
+    # Modelled somewhat after 'def importvgl':
+    #   Prompt for an existing electric lib.
+    #   Prompt for a target cellname (for both mag and electric icon).
+    # (The AS vs INTO behavior is incomplete as yet. Used so far with current-project as newname arg).
+    #   newname : target project-name (INTO), or None (AS: i.e. prompt to create one).
+    # Either newname is given: we PROMPT to pick an existing elecLib;
+    # Else PROMPT for new projectName and CREATE it (and use elecLib of same name).
+    #------------------------------------------------------------------------
+    def padframe_calc_work(self, newname=None):
+        elecLib = newname
+        isnew = not newname
+        if isnew:
+            # Use create project code first to generate a valid project space.
+            newname = self.createproject(None)
+            if not newname: return 0		# Canceled in dialog, no action.
+            # print("padframe-calc in new project " + newname + ".")
+            elecLib = newname
+
+        # For life of this projectManager process, store/recall last PadFrame Settings per project
+        global project2pfd
+        try:
+            project2pfd
+        except:
+            project2pfd = {}
+        if newname not in project2pfd:
+            project2pfd[newname] = {"libEntry": None, "cellName": None}
+
+        ppath = self.projectdir + '/' + newname
+        choices = self.get_elecLib_list(newname)
+        if not choices:
+            print( "Aborted: No existing electric libraries found to write symbol into.")
+            return 0
+                
+        elecLib = newname + '/elec/' + elecLib + '.delib'
+        elecLib = project2pfd[newname]["libEntry"] or elecLib
+        cellname = project2pfd[newname]["cellName"] or "padframe"
+        libAndCell = ExistingElecLibCellDialog(self, None, title="PadFrame Settings", plist=choices, descPost="of icon&layout", seedLibNm=elecLib, seedCellNm=cellname).result
+        if not libAndCell:
+            return 0		# Canceled in dialog, no action.
+            
+        (elecLib, cellname) = libAndCell
+        if not cellname:
+            return 0		# empty cellname, no action.
+
+        project2pfd[newname]["libEntry"] = elecLib
+        project2pfd[newname]["cellName"] = cellname
+
+        # Isolate just electric lib name without extension. ../a/b.delib -> b
+        elecLib = os.path.splitext(os.path.split(elecLib)[-1])[0]
+        print("padframe-calc in project: %s, elecLib: %s, cellName: %s" % (newname, elecLib, cellname))
+
+        export = dict(os.environ)
+        export['EF_DESIGNDIR'] = ppath
+        subprocess.Popen(['/ef/apps/bin/padframe-calc', elecLib, cellname], cwd = ppath, env = export)
+
+        # not yet any useful return value or reporting of results here in projectManager...
+        return 1
+
+    #----------------------------------------------------------------------
+    # Run the schematic editor (tool as given by user preference)
+    #----------------------------------------------------------------------
+
+    def edit_schematic(self):
+        value = self.projectselect.selected()
+        if value:
+            design = value['values'][0]
+            
+            pdktechdir = design + self.config_path(design)+'/techdir/libs.tech'
+            
+            applist = self.list_valid_schematic_editors(pdktechdir)
+
+            if len(applist)==0:
+                print("Unable to find a valid schematic editor.")
+                return
+                
+            # If the preferred app is in the list, then use it.
+            
+            if self.prefs['schemeditor'] in applist:
+                appused = self.prefs['schemeditor']
+            else:
+                appused = applist[0]
+
+            if appused == 'xcircuit':
+                return self.edit_schematic_with_xcircuit()
+            elif appused == 'xschem':
+                return self.edit_schematic_with_xschem()
+            elif appused == 'electric':
+                return self.edit_schematic_with_electric()
+            else:
+                print("Unknown/unsupported schematic editor " + appused + ".", file=sys.stderr)
+
+        else:
+            print("You must first select a project.", file=sys.stderr)
+
+    #----------------------------------------------------------------------
+    # Run the schematic editor (electric)
+    #----------------------------------------------------------------------
+
+    def edit_schematic_with_electric(self):
+        value = self.projectselect.selected()
+        if value:
+            design = value['values'][0]
+            # designname = value['text']
+            # self.project_name set by setcurrent.  This is the true project
+            # name, as opposed to the directory name.
+            designname = self.project_name
+            print('Edit schematic ' + designname + ' (' + design + ' )')
+            # Collect libs on command-line;  electric opens these in Explorer
+            libs = []
+            ellibrex = re.compile(r'^(tech_.*|ef_examples)\.[dj]elib$', re.IGNORECASE)
+
+            self.reinitElec(design)
+
+            # /elec and /.java are prerequisites for running electric
+            if not os.path.exists(design + '/elec'):
+                print("No path to electric design folder.")
+                return
+
+            if not os.path.exists(design + '/elec/.java'):
+                print("No path to electric .java folder.")
+                return
+
+            # Fix the LIBDIRS file if needed
+            #fix_libdirs(design, create = True)
+
+            # Check for legacy directory (missing .ef-config and/or .ef-config/techdir);
+            # Handle as necessary.
+
+            # don't sometimes yield pdkdir as some subdir of techdir
+            pdkdir = design + self.config_path(design) + '/techdir/'
+            if not os.path.exists(pdkdir):
+                export = dict(os.environ)
+                export['EF_DESIGNDIR'] = design
+                '''
+                p = subprocess.run(['/ef/efabless/bin/ef-config', '-sh', '-t'],
+			stdout = subprocess.PIPE, env = export)
+                config_out = p.stdout.splitlines()
+                for line in config_out:
+                    setline = line.decode('utf-8').split('=')
+                    if setline[0] == 'EF_TECHDIR':
+                        pdkdir = re.sub("[';]", "", setline[1])
+                '''
+
+            for subpath in ('libs.tech/elec/', 'libs.ref/elec/'):
+                pdkelec = os.path.join(pdkdir, subpath)
+                if os.path.exists(pdkelec) and os.path.isdir(pdkelec):
+                    # don't use os.walk(), it is recursive, wastes time
+                    for entry in os.scandir(pdkelec):
+                        if ellibrex.match(entry.name):
+                                libs.append(entry.path)
+
+            # Locate most useful project-local elec-lib to open on electric cmd-line.
+            designroot = os.path.split(design)[1]
+            finalInDesDirLibAdded = False
+            if os.path.exists(design + '/elec/' + designname + '.jelib'):
+                libs.append(design + '/elec/' + designname + '.jelib')
+                finalInDesDirLibAdded = True
+            elif os.path.isdir(design + '/elec/' + designname + '.delib'):
+                libs.append(design + '/elec/' + designname + '.delib')
+                finalInDesDirLibAdded = True
+            else:
+                # Alternative path is the project name + .delib
+                if os.path.isdir(design + '/elec/' + designroot + '.delib'):
+                    libs.append(design + '/elec/' + designroot + '.delib')
+                    finalInDesDirLibAdded = True
+
+            # Finally, check for the one absolute requirement for a project,
+            # which is that there must be a symbol designname + .ic in the
+            # last directory.  If not, then do a search for it.
+            if not finalInDesDirLibAdded or not os.path.isfile(libs[-1] + '/' + designname + '.ic'):
+                delibdirs = os.listdir(design + '/elec')
+                for delibdir in delibdirs:
+                    if os.path.splitext(delibdir)[1] == '.delib':
+                        iconfiles = os.listdir(design + '/elec/' + delibdir)
+                        for iconfile in iconfiles:
+                            if iconfile == designname + '.ic':
+                                libs.append(design + '/elec/' + delibdir)
+                                finalInDesDirLibAdded = True
+                                break
+            
+            # Above project-local lib-adds are all conditional on finding some lib
+            # with an expected name or content: all of which may fail.
+            # Force last item ALWAYS to be 'a path' in the project's elec/ dir.
+            # Usually it's a real library (found above). (If lib does not exist the messages
+            # window does get an error message). But the purpose is for the universal side-effect:
+            # To EVERY TIME reseed the File/OpenLibrary dialogue WorkDir to start in
+            # project's elec/ dir; avoid it starting somewhere in the PDK, which
+            # is what will happen if last actual cmd-line arg is a lib in the PDK, and
+            # about which users have complained. (Optimal fix needs electric enhancement).
+            if not finalInDesDirLibAdded:
+                libs.append(design + '/elec/' + designroot + '.delib')
+
+            # Pull last item from libs and make it a command-line argument.
+            # All other libraries become part of the EOPENARGS environment variable,
+            # and electric is called with the elecOpen.bsh script.
+            indirectlibs = libs[:-1]
+            export = dict(os.environ)
+            arguments = []
+            if indirectlibs:
+                export['EOPENARGS'] = ' '.join(indirectlibs)
+                arguments.append('-s')
+                arguments.append('/ef/efabless/lib/elec/elecOpen.bsh')
+
+            try:
+                arguments.append(libs[-1])
+            except IndexError:
+                print('Error:  Electric project directories not set up correctly?')
+            else:
+                subprocess.Popen(['electric', *arguments], cwd = design + '/elec',
+				env = export)
+        else:
+            print("You must first select a project.", file=sys.stderr)
+
+    #----------------------------------------------------------------------
+    # Run the schematic editor (xcircuit)
+    #----------------------------------------------------------------------
+
+    def edit_schematic_with_xcircuit(self):
+        value = self.projectselect.selected()
+        if value:
+            design = value['values'][0]
+            # designname = value['text']
+            # self.project_name set by setcurrent.  This is the true project
+            # name, as opposed to the directory name.
+            designname = self.project_name
+            print('Edit schematic ' + designname + ' (' + design + ' )')
+            xcircdirpath = design + '/xcirc'
+            pdkdir = design + self.config_path(design) + '/techdir/libs.tech/xcircuit'
+
+            # /xcirc directory is a prerequisite for running xcircuit.  If it doesn't
+            # exist, create it and seed it with .xcircuitrc from the tech directory
+            if not os.path.exists(xcircdirpath):
+                os.makedirs(xcircdirpath)
+
+            # Copy xcircuit startup file from tech directory
+            hasxcircrcfile = os.path.exists(xcircdirpath + '/.xcircuitrc')
+            if not hasxcircrcfile:
+                if os.path.exists(pdkdir + '/xcircuitrc'):
+                    shutil.copy(pdkdir + '/xcircuitrc', xcircdirpath + '/.xcircuitrc')
+
+            # Command line argument is the project name
+            arguments = [design + '/xcirc' + designname]
+            subprocess.Popen(['xcircuit', *arguments])
+        else:
+            print("You must first select a project.", file=sys.stderr)
+
+    #----------------------------------------------------------------------
+    # Run the schematic editor (xschem)
+    #----------------------------------------------------------------------
+
+    def edit_schematic_with_xschem(self):
+        value = self.projectselect.selected()
+        if value:
+            design = value['values'][0]
+            # self.project_name set by setcurrent.  This is the true project
+            # name, as opposed to the directory name.
+            designname = self.project_name
+            print('Edit schematic ' + designname + ' (' + design + ' )')
+            xschemdirpath = design + '/xschem'
+            
+            pdkdir = design + self.config_path(design) + '/techdir/libs.tech/xschem'
+
+            
+            # /xschem directory is a prerequisite for running xschem.  If it doesn't
+            # exist, create it and seed it with xschemrc from the tech directory
+            if not os.path.exists(xschemdirpath):
+                os.makedirs(xschemdirpath)
+
+            # Copy xschem startup file from tech directory
+            hasxschemrcfile = os.path.exists(xschemdirpath + '/xschemrc')
+            if not hasxschemrcfile:
+                if os.path.exists(pdkdir + '/xschemrc'):
+                    shutil.copy(pdkdir + '/xschemrc', xschemdirpath + '/xschemrc')
+
+            # Command line argument is the project name.  The "-r" option is recommended if there
+            # is no stdin/stdout piping.
+            
+            arguments = ['-r', design + '/xschem/' + designname]
+            subprocess.Popen(['xschem', *arguments])
+        else:
+            print("You must first select a project.", file=sys.stderr)
+
+    #----------------------------------------------------------------------
+    # Run the layout editor (magic or klayout)
+    #----------------------------------------------------------------------
+
+    def edit_layout(self):
+        value = self.projectselect.selected()
+        if value:
+            design = value['values'][0]
+            pdktechdir = design + self.config_path(design) + '/techdir/libs.tech'
+            
+            applist = self.list_valid_layout_editors(pdktechdir)
+
+            if len(applist)==0:
+                print("Unable to find a valid layout editor.")
+                return
+
+            # If the preferred app is in the list, then use it.
+            if self.prefs['layouteditor'] in applist:
+                appused = self.prefs['layouteditor']
+            else:
+                appused = applist[0]
+
+            if appused == 'magic':
+                return self.edit_layout_with_magic()
+            elif appused == 'klayout':
+                return self.edit_layout_with_klayout()
+            elif appused == 'electric':
+                return self.edit_layout_with_electric()
+            else:
+                print("Unknown/unsupported layout editor " + appused + ".", file=sys.stderr)
+
+        else:
+            print("You must first select a project.", file=sys.stderr)
+
+    #----------------------------------------------------------------------
+    # Run the magic layout editor
+    #----------------------------------------------------------------------
+
+    def edit_layout_with_magic(self):
+        value = self.projectselect.selected()
+        if value:
+            design = value['values'][0]
+            # designname = value['text']
+            designname = self.project_name
+            
+            pdkdir = ''
+            pdkname = ''
+            
+            if os.path.exists(design + '/.ef-config/techdir/libs.tech'):
+                pdkdir = design + '/.ef-config/techdir/libs.tech/magic/current'
+                pdkname = os.path.split(os.path.realpath(design + '/.ef-config/techdir'))[1]
+            elif os.path.exists(design + '/.config/techdir/libs.tech'):
+                pdkdir = design + '/.config/techdir/libs.tech/magic'
+                pdkname = os.path.split(os.path.realpath(design + '/.config/techdir'))[1]
+            
+
+            # Check if the project has a /mag directory.  Create it and
+            # put the correct .magicrc file in it, if it doesn't.
+            magdirpath = design + '/mag'
+            hasmagdir = os.path.exists(magdirpath)
+            if not hasmagdir:
+                os.makedirs(magdirpath)
+
+            hasmagrcfile = os.path.exists(magdirpath + '/.magicrc')
+            if not hasmagrcfile:
+                shutil.copy(pdkdir + '/' + pdkname + '.magicrc', magdirpath + '/.magicrc')
+
+            # Check if the .mag file exists for the project.  If not,
+            # generate a dialog.
+            magpath = design + '/mag/' + designname + '.mag'
+            netpath = design + '/spi/' + designname + '.spi'
+            # print("magpath is " + magpath)
+            hasmag = os.path.exists(magpath)
+            hasnet = os.path.exists(netpath)
+            if hasmag:
+                if hasnet:
+                    statbuf1 = os.stat(magpath)
+                    statbuf2 = os.stat(netpath)
+                    # No specific action for out-of-date layout.  To be done:
+                    # Check contents and determine if additional devices need to
+                    # be added to the layout.  This may be more trouble than it's
+                    # worth.
+                    #
+                    # if statbuf2.st_mtime > statbuf1.st_mtime:
+                    #     hasmag = False
+
+            if not hasmag:
+                # Does the project have any .mag files at all?  If so, the project
+                # layout may be under a name different than the project name.  If
+                # so, present the user with a selectable list of layout names,
+                # with the option to start a new layout or import from schematic.
+
+                maglist = os.listdir(design + '/mag/')
+                if len(maglist) > 1:
+                    # Generate selection menu
+                    warning = 'No layout matches IP name ' + designname + '.'
+                    maglist = list(item for item in maglist if os.path.splitext(item)[1] == '.mag')
+                    clist = list(os.path.splitext(item)[0] for item in maglist)
+                    ppath = EditLayoutDialog(self, clist, ppath=design,
+					pname=designname, warning=warning,
+					hasnet=hasnet).result
+                    if not ppath:
+                        return 0		# Canceled in dialog, no action.
+                    elif ppath != '(New layout)':
+                        hasmag = True
+                        designname = ppath
+                elif len(maglist) == 1:
+                    # Only one magic file, no selection, just bring it up.
+                    designname = os.path.split(maglist[0])[1]
+                    hasmag = True
+
+            if not hasmag:
+                populate = NewLayoutDialog(self, "No layout for project.").result
+                if not populate:
+                    return 0	# Canceled, no action.
+                elif populate():
+                    # Name of PDK deprecated.  The .magicrc file in the /mag directory
+                    # will load the correct PDK and specify the proper library for the
+                    # low-level device namespace, which may not be the same as techdir.
+                    # NOTE:  netlist_to_layout script will attempt to generate a
+                    # schematic netlist if one does not exist.
+
+                    print('Running /ef/efabless/bin/netlist_to_layout.py ../spi/' + designname + '.spi')
+                    try:
+                        p = subprocess.run(['/ef/efabless/bin/netlist_to_layout.py',
+					'../spi/' + designname + '.spi'],
+					stdin = subprocess.PIPE, stdout = subprocess.PIPE,
+					stderr = subprocess.PIPE, cwd = design + '/mag')
+                        if p.stderr:
+                            err_string = p.stderr.splitlines()[0].decode('utf-8')
+                            # Print error messages to console
+                            print(err_string)
+
+                    except subprocess.CalledProcessError as e:
+                        print('Error running netlist_to_layout.py: ' + e.output.decode('utf-8'))
+                    else:
+                        if os.path.exists(design + '/mag/create_script.tcl'):
+                            with open(design + '/mag/create_script.tcl', 'r') as infile:
+                                magproc = subprocess.run(['/ef/apps/bin/magic',
+					'-dnull', '-noconsole', '-rcfile ',
+					pdkdir + '/' + pdkname + '.magicrc', designname],
+					stdin = infile, stdout = subprocess.PIPE,
+					stderr = subprocess.PIPE, cwd = design + '/mag')
+                            print("Populated layout cell")
+                            # os.remove(design + '/mag/create_script.tcl')
+                        else:
+                            print("No device generating script was created.", file=sys.stderr)
+
+            print('Edit layout ' + designname + ' (' + design + ' )')
+
+            magiccommand = ['magic']
+            # Select the graphics package used by magic from the profile settings.
+            if 'magic-graphics' in self.prefs:
+                magiccommand.extend(['-d', self.prefs['magic-graphics']])
+            # Check if .magicrc predates the latest and warn if so.
+            statbuf1 = os.stat(design + '/mag/.magicrc')
+            statbuf2 = os.stat(pdkdir + '/' + pdkname + '.magicrc')
+            if statbuf2.st_mtime > statbuf1.st_mtime:
+                print('NOTE:  File .magicrc predates technology startup file.  Using default instead.')
+                magiccommand.extend(['-rcfile', pdkdir + '/' + pdkname + '.magicrc'])
+            magiccommand.append(designname)
+
+            # Run magic and don't wait for it to finish
+            subprocess.Popen(magiccommand, cwd = design + '/mag')
+        else:
+            print("You must first select a project.", file=sys.stderr)
+
+    #----------------------------------------------------------------------
+    # Run the klayout layout editor
+    #----------------------------------------------------------------------
+
+    def edit_layout_with_klayout(self):
+        value = self.projectselect.selected()
+        print("Klayout unsupported from project manager (work in progress);  run manually", file=sys.stderr)
+
+    #----------------------------------------------------------------------
+    # Run the electric layout editor
+    #----------------------------------------------------------------------
+
+    def edit_layout_with_electric(self):
+        value = self.projectselect.selected()
+        print("Electric layout editing unsupported from project manager (work in progress);  run manually", file=sys.stderr)
+
+    #----------------------------------------------------------------------
+    # Upload design to the marketplace
+    # NOTE:  This is not being called by anything.  Use version in the
+    # characterization script, which can check for local results before
+    # approving (or forcing) an upload.
+    #----------------------------------------------------------------------
+
+    def upload(self):
+        '''
+        value = self.projectselect.selected()
+        if value:
+            design = value['values'][0]
+            # designname = value['text']
+            designname = self.project_name
+            print('Upload design ' + designname + ' (' + design + ' )')
+            subprocess.run(['/ef/apps/bin/withnet',
+			og_config.apps_path + '/cace_design_upload.py',
+			design, '-test'])
+	'''
+
+    #--------------------------------------------------------------------------
+    # Upload a datasheet to the marketplace (Administrative use only, for now)
+    #--------------------------------------------------------------------------
+
+    # def make_challenge(self):
+    #      importp = self.cur_import
+    #      print("Make a Challenge from import " + importp + "!")
+    #      # subprocess.run([og_config.apps_path + '/cace_import_upload.py', importp, '-test'])
+
+    def setcurrent(self, value):
+        global currdesign
+        treeview = value.widget
+        selection = treeview.item(treeview.selection())
+        pname = selection['text']
+        #print("setcurrent returned value " + pname)
+        efmetapath = os.path.expanduser(currdesign)
+        if not os.path.exists(efmetapath):
+            os.makedirs(os.path.split(efmetapath)[0], exist_ok=True)
+        with open(efmetapath, 'w') as f:
+            f.write(pname + '\n')
+
+        # Pick up the PDK from "values", use it to find the PDK folder, determine
+        # if it has a "magic" subfolder, and enable/disable the "Edit Layout"
+        # button accordingly
+        
+        svalues = selection['values']
+        pdkitems = svalues[1].split()
+        pdkdir = ''
+        
+        ef_style=False
+        
+        if os.path.exists(svalues[0] + '/.config'):
+            pdkdir = svalues[0] + '/.config/techdir'
+        elif os.path.exists(svalues[0] + '/.ef-config'):
+            pdkdir = svalues[0] + '/.ef-config/techdir'
+            ef_style=True
+        
+        if pdkdir == '':
+            print('No pdkname found; layout editing disabled')
+            self.toppane.appbar.layout_button.config(state='disabled')
+        else:
+            try:
+                if ef_style:
+                    subf = os.listdir(pdkdir + '/libs.tech/magic/current')
+                else:
+                    subf = os.listdir(pdkdir + '/libs.tech/magic')
+            except:
+                print('PDK ' + pdkname + ' has no layout setup; layout editing disabled')
+                self.toppane.appbar.layout_button.config(state='disabled') 
+        '''
+        svalues = selection['values'][1]
+        print('selection: '+str(selection))
+        pdkitems = svalues.split()
+        print('Contents of pdkitems: '+str(pdkitems))
+        pdkname = ''
+        if ':' in pdkitems:
+            pdkitems.remove(':')
+        if len(pdkitems) == 2:
+            # New behavior Sept. 2017, have to cope with <foundry>.<N> directories, ugh.
+            pdkdirs = os.listdir('/usr/share/pdk/')
+            #TODO: pdkdirs = os.listdir('PREFIX/pdk/')
+            
+            for pdkdir in pdkdirs:
+                if pdkitems[0] == pdkdir:
+                    pdkname = pdkdir
+                    #TODO: PREFIX
+                    if os.path.exists('/usr/share/pdk/' + pdkname + '/' + pdkitems[1]):
+                        break
+                else:
+                    pdkpair = pdkdir.split('.')
+                    if pdkpair[0] == pdkitems[0]:
+                        pdkname = pdkdir
+                        #TODO: PREFIX
+                        if os.path.exists('/usr/share/pdk/' + pdkname + '/' + pdkitems[1]):
+                            break
+            if pdkname == '':
+                print('No pdkname found; layout editing disabled')
+                self.toppane.appbar.layout_button.config(state='disabled')
+            else:
+                try:
+                    subf = os.listdir('/ef/tech/' + pdkname + '/' + pdkitems[1] + '/libs.tech/magic/current')
+                except:
+                    print('PDK ' + pdkname + ' has no layout setup; layout editing disabled')
+                    self.toppane.appbar.layout_button.config(state='disabled')
+                else:
+                    self.toppane.appbar.layout_button.config(state='enabled')
+        else:
+            print('No PDK returned in project selection data;  layout editing disabled.')
+            self.toppane.appbar.layout_button.config(state='disabled')
+        '''
+        # If the selected project directory has a JSON file and netlists in the "spi"
+        # and "testbench" folders, then enable the "Characterize" button;  else disable
+        # it.
+        # NOTE:  project.json is the preferred name for the datasheet
+        # file.  However, the .spi file, .delib file, etc., all have the name of the
+        # project from "ip-name" in the datasheet.
+        # "<project_folder_name>.json" is the legacy name for the datasheet, deprecated.
+
+        found = False
+        ppath = selection['values'][0]
+        jsonname = ppath + '/project.json'
+        legacyname = ppath + '/' + pname + '.json'
+        if not os.path.isfile(jsonname):
+            if os.path.isfile(legacyname):
+                jsonname = legacyname
+
+        if os.path.isfile(jsonname):
+            # Pull the ipname into local store (may want to do this with the
+            # datasheet as well)
+            with open(jsonname, 'r') as f:
+                datatop = json.load(f)
+                dsheet = datatop['data-sheet']
+                ipname = dsheet['ip-name']
+                self.project_name = ipname
+                found = True
+       
+            # Do not specifically prohibit opening the characterization app if
+            # there is no schematic or netlist.  Otherwise the user is prevented
+            # even from seeing the electrical parameters.  Let the characterization
+            # tool allow or prohibit simulation based on this.
+            # if os.path.exists(ppath + '/spi'):
+            #     if os.path.isfile(ppath + '/spi/' + ipname + '.spi'):
+            #         found = True
+            #
+            # if found == False and os.path.exists(ppath + '/elec'):
+            #     if os.path.isdir(ppath + '/elec/' + ipname + '.delib'):
+            #         if os.path.isfile(ppath + '/elec/' + ipname + '.delib/' + ipname + '.sch'):
+            #             found = True
+        else:
+            # Use 'pname' as the default project name.
+            print('No characterization file ' + jsonname)
+            print('Setting project ip-name from the project folder name.')
+            self.project_name = pname
+
+        # If datasheet has physical parameters but not electrical parameters, then it's okay
+        # for it not to have a testbench directory;  it's still valid.  However, having
+        # neither physical nor electrical parameters means there's nothing to characterize.
+        if found and 'electrical-params' in dsheet and len(dsheet['electrical-params']) > 0:
+            if not os.path.isdir(ppath + '/testbench'):
+                print('No testbench directory for eletrical parameter simulation methods.', file=sys.stderr)
+                found = False
+        elif found and not 'physical-params' in dsheet:
+            print('Characterization file defines no characterization tests.', file=sys.stderr)
+            found = False
+        elif found and 'physical-params' in dsheet and len(dsheet['physical-params']) == 0:
+            print('Characterization file defines no characterization tests.', file=sys.stderr)
+            found = False
+
+        if found == True:
+            self.toppane.appbar.char_button.config(state='enabled')
+        else:
+            self.toppane.appbar.char_button.config(state='disabled')
+
+        # Warning: temporary hack (Tim, 1/9/2018)
+        # Pad frame generator is currently limited to the XH035 cells, so if the
+        # project PDK is not XH035, disable the pad frame button
+
+        if len(pdkitems) > 1 and pdkitems[1] == 'EFXH035B':
+            self.toppane.appbar.padframeCalc_button.config(state='enabled')
+        else:
+            self.toppane.appbar.padframeCalc_button.config(state='disabled')
+
+# main app. fyi: there's a 2nd/earlier __main__ section for splashscreen
+if __name__ == '__main__':
+    OpenGalaxyManager(root)
+    if deferLoad:
+        # Without this, mainloop may find&run very short clock-delayed events BEFORE main form display.
+        # With it 1st project-load can be scheduled using after-time=0 (needn't tune a delay like 100ms).
+        root.update_idletasks()
+    root.mainloop()