updated startup copyright notice
[hooke.git] / hooke_cli.py
1 #!/usr/bin/env python
2
3 '''
4 hooke_cli.py
5
6 Command line module of Hooke.
7
8 Copyright (C) 2006 Massimo Sandal (University of Bologna, Italy).
9
10 This program is released under the GNU General Public License version 2.
11 '''
12
13
14 from libhooke import * #FIXME
15 import libhookecurve as lhc
16
17 import libinput as linp
18 import liboutlet as lout
19
20 from libhooke import WX_GOOD
21 from libhooke import HOOKE_VERSION
22
23 import wxversion
24 wxversion.select(WX_GOOD)
25 import wx
26
27 from wx.lib.newevent import NewEvent
28 from matplotlib.numerix import * #FIXME
29
30 import xml.dom.minidom
31 import sys, os, os.path, glob, shutil
32 import Queue
33 import cmd
34 import time
35
36 global __version__
37 global __codename__
38 global __releasedate__
39 __version__ = HOOKE_VERSION[0]
40 __codename__ = HOOKE_VERSION[1]
41 __releasedate__ = HOOKE_VERSION[2]
42
43 from matplotlib import __version__ as mpl_version
44 from wx import __version__ as wx_version
45 from wxmpl import __version__ as wxmpl_version
46 from scipy import __version__ as scipy_version
47 from numpy import __version__ as numpy_version
48 from sys import version as python_version
49 import platform
50
51
52 class HookeCli(cmd.Cmd):
53     
54     def __init__(self,frame,list_of_events,events_from_gui,config,drivers):
55         cmd.Cmd.__init__(self)
56                        
57         self.prompt = 'hooke: '
58         
59         
60         self.current_list=[] #the playlist we're using
61         
62         self.current=None    #the current curve under analysis. 
63         self.plots=None
64         '''
65         The actual hierarchy of the "current curve" is a bit complex:
66         
67         self.current = the lhc.HookeCurve container object of the current curve
68         self.current.curve = the current "real" curve object as defined in the filetype driver class
69         self.current.curve.default_plots() = the default plots of the filetype driver.
70         
71         The plot objects obtained by mean of self.current.curve.default_plots() 
72         then undergoes modifications by the plotmanip
73         modifier functions. The modified plot is saved in self.plots and used if needed by other functions.       
74         '''
75         
76         
77         self.pointer=0       #a pointer to navigate the current list
78                         
79         #Things that come from outside
80         self.frame=frame                        #the wx frame we refer to
81         self.list_of_events=list_of_events      #a list of wx events we use to interact with the GUI
82         self.events_from_gui=events_from_gui    #the Queue object we use to have messages from the GUI
83         self.config=config                      #the configuration dictionary
84         self.drivers=drivers                    #the file format drivers
85         
86         #get plot manipulation functions
87         plotmanip_functions=[]
88         for object_name in dir(self):
89                 if object_name[0:9]=='plotmanip':
90                     plotmanip_functions.append(getattr(self,object_name))
91         #put plotmanips in order
92         self.plotmanip=[None for item in self.config['plotmanips']]
93         for item in plotmanip_functions:
94             namefunction=item.__name__[10:]
95             if namefunction in self.config['plotmanips']:
96                 nameindex=self.config['plotmanips'].index(namefunction) #index of function in plotmanips config
97                 self.plotmanip[nameindex] = item
98             else:
99                 pass
100            
101             
102         self.playlist_saved=0
103         self.playlist_name=''
104         self.notes_saved=1
105
106         #create outlet
107         self.outlet=lout.Outlet()
108         
109         #Data that must be saved in the playlist, related to the whole playlist (not individual curves)
110         self.playlist_generics={} 
111         
112         #make sure we execute _plug_init() for every command line plugin we import
113         for plugin_name in self.config['plugins']:
114             try:
115                 plugin=__import__(plugin_name)
116                 try:
117                     eval('plugin.'+plugin_name+'Commands._plug_init(self)')
118                 except AttributeError:
119                     pass
120             except ImportError:
121                 pass
122
123         
124 #HELPER FUNCTIONS
125 #Everything sending an event should be here
126     def _measure_N_points(self, N, whatset=1):
127         '''
128         general helper function for N-points measures
129         '''
130         measure_points=self.list_of_events['measure_points']
131         wx.PostEvent(self.frame, measure_points(num_of_points=N, set=whatset))
132         while 1:
133             try:
134                 points=self.frame.events_from_gui.get()
135                 break
136             except Empty:
137                 pass
138         return points
139         
140     def _get_displayed_plot(self,dest=0):
141         '''
142         returns the currently displayed plot.
143         '''
144         wx.PostEvent(self.frame, self.list_of_events['get_displayed_plot'](dest=dest))
145         while 1:
146             try:
147                 displayed_plot=self.events_from_gui.get()
148             except Empty:
149                 pass
150             if displayed_plot:
151                 break
152         return displayed_plot
153     
154     def _send_plot(self,plots):
155         '''
156         sends a plot to the GUI
157         '''
158         wx.PostEvent(self.frame, self.list_of_events['plot_graph'](plots=plots))
159         return
160         
161     def _find_plotmanip(self, name):
162         '''
163         returns a plot manipulator function from its name
164         '''
165         return self.plotmanip[self.config['plotmanips'].index(name)]
166     
167 #HERE COMMANDS BEGIN
168     
169     def help_set(self):
170         print '''
171 SET
172 Sets a local configuration variable
173 -------------
174 Syntax: set [variable] [value]
175         '''
176     def do_set(self,args):
177         #FIXME: some variables in self.config should be hidden or intelligently configurated...
178         args=args.split()
179         if len(args)==0:
180             print 'You must specify a variable and a value'
181             print 'Available variables:'
182             print self.config.keys()
183             return
184         if args[0] not in self.config.keys():
185             print 'This is not an internal Hooke variable!'
186             return
187         if len(args)==1:
188             #FIXME:we should reload the config file and reset the config value
189             print self.config[args[0]]
190             return
191         key=args[0]
192         try: #try to have a numeric value
193             value=float(args[1])
194         except ValueError: #if it cannot be converted to float, it's None, or a string...
195             if value.lower()=='none':
196                 value=None
197             else:
198                 value=args[1]
199                 
200         self.config[key]=value
201         self.do_plot(0)
202         
203 #PLAYLIST MANAGEMENT AND NAVIGATION
204 #------------------------------------
205     
206     def help_loadlist(self):
207         print '''
208 LOADLIST
209 Loads a file playlist
210 -----------
211 Syntax: loadlist [playlist file]
212         '''
213     def do_loadlist(self, args):
214         #checking for args: if nothing is given as input, we warn and exit.
215         while len(args)==0:
216             args=linp.alphainput('File to load?','',0,[])
217         
218         arglist=args.split()
219         play_to_load=arglist[0]
220         
221         #We assume a Hooke playlist has the extension .hkp
222         if play_to_load[-4:] != '.hkp':
223             play_to_load+='.hkp'
224         
225         try:            
226             playxml=PlaylistXML()
227             self.current_list, self.playlist_generics=playxml.load(play_to_load)
228             self.current_playxml=playxml
229         except IOError:
230             print 'File not found.'
231             return
232         
233         print 'Loaded %s curves' %len(self.current_list)
234         
235         if 'pointer' in self.playlist_generics.keys():
236             self.pointer=int(self.playlist_generics['pointer'])
237         else:
238             #if no pointer is found, set the current curve as the first curve of the loaded playlist
239             self.pointer=0
240         print 'Starting at curve ',self.pointer
241             
242         self.current=self.current_list[self.pointer]
243         
244         #resets saved/notes saved state
245         self.playlist_saved=0
246         self.playlist_name=''
247         self.notes_saved=0        
248     
249         self.do_plot(0)
250         
251         
252     def help_genlist(self):
253         print '''
254 GENLIST
255 Generates a file playlist.
256 Note it doesn't *save* it: see savelist for this.
257
258 If [input files] is a directory, it will use all files in the directory for playlist.
259 So:
260 genlist dir
261 genlist dir/
262 genlist dir/*.*
263
264 are all equivalent syntax.
265 ------------
266 Syntax: genlist [input files]
267         
268 '''
269     def do_genlist(self,args):
270         #args list is: input path, output name
271         if len(args)==0:
272             args=linp.alphainput('Input files?','',1,[])
273                     
274         arglist=args.split()      
275         list_path=arglist[0]
276                   
277         #if it's a directory, is like /directory/*.*
278         #FIXME: probably a bit kludgy.
279         if os.path.isdir(list_path): 
280             if platform.system == 'Windows':
281                 SLASH="\\"
282             else:
283                 SLASH="/"
284             if list_path[-1] == SLASH:
285                 list_path=list_path+'*.*'
286             else:    
287                 list_path=list_path+SLASH+'*.*'
288         
289         #expanding correctly the input list with the glob module :)        
290         list_files=glob.glob(list_path)
291         list_files.sort()
292
293         self.current_list=[]
294         for item in list_files:
295             try:
296                 self.current_list.append(lhc.HookeCurve(os.path.abspath(item))) 
297             except:
298                 pass
299             
300         self.pointer=0    
301         if len(self.current_list)>0:
302             self.current=self.current_list[self.pointer]
303         else:
304             print 'Empty list!'
305             return
306         
307         #resets saved/notes saved state
308         self.playlist_saved=0
309         self.playlist_name=''
310         self.notes_saved=0  
311         
312         self.do_plot(0)
313        
314         
315     def do_savelist(self,args):
316         '''
317         SAVELIST
318         Saves the current file playlist on disk.
319         ------------
320         Syntax: savelist [filename]
321         '''
322         while len(args)==0:
323             args=linp.alphainput('Output files?','',1,[])
324     
325         output_filename=args
326         
327         self.playlist_generics['pointer']=self.pointer
328         
329         #autocomplete filename if not specified
330         if output_filename[-4:] != '.hkp':
331             output_filename+='.hkp'
332         
333         playxml=PlaylistXML()
334         playxml.export(self.current_list, self.playlist_generics)
335         playxml.save(output_filename)                  
336         
337         #remembers we have saved playlist
338         self.playlist_saved=1
339         
340     def help_addtolist(self):
341         print '''
342 ADDTOLIST
343 Adds a file to the current playlist
344 --------------
345 Syntax: addtolist [filename]
346 '''
347     def do_addtolist(self,args):
348         #args list is: input path
349         if len(args)==0:
350             print 'You must give the input filename you want to add'
351             self.help_addtolist()
352             return
353           
354         filenames=glob.glob(args)
355         
356         for filename in filenames:
357             self.current_list.append(lhc.HookeCurve(os.path.abspath(filename)))
358         #we need to save playlist
359         self.playlist_saved=0
360     
361     def help_printlist(self):
362         print '''
363 PRINTLIST
364 Prints the list of curves in the current playlist
365 -------------
366 Syntax: printlist
367 '''
368     def do_printlist(self,args):
369         for item in self.current_list:
370             print item.path
371             
372     
373     def help_jump(self):
374         print '''
375 JUMP
376 Jumps to a given curve.
377 ------
378 Syntax: jump {$curve}
379
380 If the curve is not in the current playlist, it politely asks if we want to add it.
381         '''  
382     def do_jump(self,filename):
383         '''
384         jumps to the curve with the given filename.
385         if the filename is not in the playlist, it asks if we must add it or not.
386         '''
387         
388         if filename=='':
389             filename=linp.alphainput('Jump to?','',0,[])
390             
391         filepath=os.path.abspath(filename)
392         print filepath
393                 
394         c=0
395         item_not_found=1
396         while item_not_found:
397             try:
398                 
399                 if self.current_list[c].path == filepath:
400                     self.pointer=c
401                     self.current=self.current_list[self.pointer]
402                     item_not_found=0
403                     self.do_plot(0)
404                 else:
405                     c+=1  
406             except IndexError:
407                 #We've found the end of the list.
408                 answer=linp.alphainput('Curve not found in playlist. Add it to list?','y',0,[])
409                 if answer.lower()[0]=='y':
410                     try:
411                         self.do_addtolist(filepath)
412                     except:
413                         print 'Curve file not found.'
414                         return
415                     self.current=self.current_list[-1]
416                     self.pointer=(len(current_list)-1)
417                     self.do_plot(0)
418                     
419                 item_not_found=0
420     
421     
422     def do_index(self,args):
423         '''
424         INDEX
425         Prints the index of the current curve in the list
426         -----
427         Syntax: index
428         '''
429         print self.pointer+1, 'of', len(self.current_list) 
430     
431     
432     def help_next(self):
433         print '''
434 NEXT
435 Go the next curve in the playlist.
436 If we are at the last curve, we come back to the first.
437 -----
438 Syntax: next, n
439         '''
440     def do_next(self,args):
441         self.current.curve.close_all()
442         if self.pointer == (len(self.current_list)-1):
443             self.pointer=0
444             print 'Playlist finished; back to first curve.'
445         else:
446             self.pointer+=1
447         
448         self.current=self.current_list[self.pointer]
449         self.do_plot(0)
450         
451     
452     def help_n(self):
453         self.help_next()
454     def do_n(self,args):
455         self.do_next(args)
456         
457     def help_previous(self,args):
458         print '''
459 PREVIOUS
460 Go to the previous curve in the playlist.
461 If we are at the first curve, we jump to the last.
462 -------
463 Syntax: previous, p
464     '''
465     def do_previous(self,args):
466         self.current.curve.close_all()
467         if self.pointer == 0:
468             self.pointer=(len(self.current_list)-1)
469             print 'Start of playlist; jump to last curve.' 
470         else:
471             self.pointer-=1
472             
473         self.current=self.current_list[self.pointer]
474         self.do_plot(args)
475         
476             
477     def help_p(self):
478         self.help_previous()
479     def do_p(self,args):
480         self.do_previous(args)
481
482         
483 #PLOT INTERACTION COMMANDS
484 #-------------------------------    
485     def help_plot(self):
486         print '''
487 PLOT
488 Plots the current force curve
489 -------
490 Syntax: plot
491         '''
492     def do_plot(self,args):
493         
494         self.current.identify(self.drivers)
495         self.plots=self.current.curve.default_plots()
496         try:
497             self.plots=self.current.curve.default_plots()
498         except Exception, e:
499             print 'Unexpected error occurred in do_plot().'
500             print e
501             return
502             
503         #apply the plotmanip functions eventually present
504         nplots=len(self.plots)
505         c=0
506         while c<nplots:
507             for function in self.plotmanip: #FIXME: something strange happens about self.plotmanip[0]
508                 self.plots[c]=function(self.plots[c], self.current)
509                 
510             self.plots[c].xaxes=self.config['xaxes'] #FIXME: in the future, xaxes and yaxes should be set per-plot
511             self.plots[c].yaxes=self.config['yaxes']
512                 
513             c+=1
514
515         self._send_plot(self.plots)
516         
517     def _delta(self, set=1):
518         '''
519         calculates the difference between two clicked points
520         '''
521         print 'Click two points'
522         points=self._measure_N_points(N=2, whatset=set)
523         dx=abs(points[0].graph_coords[0]-points[1].graph_coords[0])
524         dy=abs(points[0].graph_coords[1]-points[1].graph_coords[1])
525         unitx=self.plots[points[0].dest].units[0]
526         unity=self.plots[points[0].dest].units[1]
527         return dx,unitx,dy,unity
528         
529     def do_delta(self,args):
530         '''
531         DELTA
532         
533         Measures the delta X and delta Y between two points.
534         ----
535         Syntax: delta
536         '''
537         dx,unitx,dy,unity=self._delta()
538         print str(dx)+' '+unitx
539         print str(dy)+' '+unity
540     
541     def _point(self, set=1):
542         '''calculates the coordinates of a single clicked point'''
543
544         print 'Click one point'
545         point=self._measure_N_points(N=1, whatset=set)
546         
547         x=point[0].graph_coords[0]
548         y=point[0].graph_coords[1]
549         unitx=self.plots[point[0].dest].units[0]
550         unity=self.plots[point[0].dest].units[1]
551         return x,unitx,y,unity
552         
553     def do_point(self,args):
554         '''
555         POINT
556         
557         Returns the coordinates of a point on the graph.
558         ----
559         Syntax: point
560         '''
561         x,unitx,y,unity=self._point()
562         print str(x)+' '+unitx
563         print str(y)+' '+unity
564         to_dump='point '+self.current.path+' '+str(x)+' '+unitx+', '+str(y)+' '+unity
565         self.outlet.push(to_dump)    
566    
567         
568     def do_close(self,args=None):
569         '''
570         CLOSE
571         Closes one of the two plots. If no arguments are given, the bottom plot is closed.
572         ------
573         Syntax: close [top,bottom]
574         '''
575         if args=='top':
576             to_close=0
577         elif args=='bottom':
578             to_close=1
579         else:
580             to_close=1
581         
582         close_plot=self.list_of_events['close_plot']
583         wx.PostEvent(self.frame, close_plot(to_close=to_close))
584         
585     def do_show(self,args=None):
586         '''
587         SHOW
588         Shows both plots.
589         ''' 
590         show_plots=self.list_of_events['show_plots']
591         wx.PostEvent(self.frame, show_plots())
592        
593         
594     
595     #PLOT EXPORT AND MANIPULATION COMMANDS
596     def help_export(self):
597         print '''
598 EXPORT
599 Saves the current plot as an image file
600 ---------------
601 Syntax: export [filename] {plot to export}
602
603 The supported formats are PNG and EPS; the file extension of the filename is automatically recognized
604 and correctly exported. Resolution is (for now) fixed at 150 dpi.
605
606 If you have a multiple plot, the optional plot to export argument tells Hooke which plot you want to export. If 0, the top plot is exported. If 1, the bottom plot is exported (Exporting both plots is still to implement)
607         '''
608     def do_export(self,args):
609         #FIXME: the bottom plot doesn't have the title
610         
611         dest=0
612         if args=='':
613             name=linp.alphainput('Filename?',self.current.path+'.png',0,[])
614         else:
615             args=args.split()
616             name=args[0]
617             if len(args) > 1:
618                 dest=int(args[1]) 
619                 
620         export_image=self.list_of_events['export_image']
621         wx.PostEvent(self.frame, export_image(name=name, dest=dest))
622         
623         
624     def help_txt(self):
625         print '''
626 TXT
627 Saves the current curve as a text file
628 Columns are, in order:
629 X1 , Y1 , X2 , Y2 , X3 , Y3 ...
630
631 -------------
632 Syntax: txt [filename] {plot to export}
633         '''
634     def do_txt(self,args):
635         
636         def transposed2(lists, defval=0):
637             '''
638             transposes a list of lists, i.e. from [[a,b,c],[x,y,z]] to [[a,x],[b,y],[c,z]] without losing
639             elements
640             (by Zoran Isailovski on the Python Cookbook online)
641             '''
642             if not lists: return []
643             return map(lambda *row: [elem or defval for elem in row], *lists)
644         
645         whichplot=0
646         args=args.split()
647         if len(args)==0:
648             filename=linp.alphainput('Filename?',self.current.path+'.txt',0,[])
649         else:
650             filename=linp.checkalphainput(args[0],self.current.path+'.txt',[])
651             try:
652                 whichplot=int(args[1])
653             except:
654                 pass
655             
656         columns=[]     
657         for dataset in self.plots[whichplot].vectors:
658             for i in range(0,len(dataset)): 
659                 columns.append([])
660                 for value in dataset[i]:
661                     columns[-1].append(str(value))                   
662         
663         rows=transposed2(columns, 'nan')
664         rows=[' , '.join(item) for item in rows]
665         text='\n'.join(rows)
666         
667         txtfile=open(filename,'w+')
668         txtfile.write(text)
669         txtfile.close()
670         
671     
672     #LOGGING, REPORTING, NOTETAKING
673     def help_note(self):
674         print '''
675 NOTE
676 Writes or displays a note about the current curve.
677 If [anything] is empty, it displays the note, otherwise it adds a note.
678 The note is then saved in the playlist if you issue a savelist command
679 ---------------
680 Syntax: note [anything]        
681 '''
682     def do_note(self,args):
683         if args=='':
684             print self.current_list[self.pointer].notes
685         else:
686             #bypass UnicodeDecodeError troubles
687             try:
688                 args=args.decode('ascii')
689             except:
690                 args=args.decode('ascii','ignore')
691                 if len(args)==0:
692                     args='?'
693                     
694             self.current_list[self.pointer].notes=args
695         self.notes_saved=0
696             
697     def help_notelog(self):
698         print '''
699 NOTELOG
700 Writes a log of the notes taken during the session for the current
701 playlist
702 --------------        
703 Syntax notelog [filename]
704 '''        
705     def do_notelog(self,args):
706         
707         if len(args)==0:
708             args=linp.alphainput('Notelog filename?','notelog.txt',0,[])
709             
710         note_lines='Notes taken at '+time.asctime()+'\n'
711         for item in self.current_list:
712             if len(item.notes)>0:
713                 #FIXME: log should be justified
714                 #FIXME: file path should be truncated...
715                 note_string=(item.path+'  |  '+item.notes+'\n')
716                 note_lines+=note_string
717                 
718         try:
719             f=open(args,'a+')
720             f.write(note_lines)
721             f.close()
722         except IOError, (ErrorNumber, ErrorMessage):
723             print 'Error: notes cannot be saved. Catched exception:'
724             print ErrorMessage
725         
726         self.notes_saved=1
727
728     def help_copylog(self):
729         print '''
730 COPYLOG
731 Moves the annotated curves to another directory
732 -----------
733 Syntax copylog [directory]
734         '''
735     def do_copylog(self,args):
736         
737         if len(args)==0:
738             args=linp.alphainput('Destination directory?','',0,[])  #TODO default
739         
740         mydir=os.path.abspath(args)
741         if not os.path.isdir(mydir):
742             print 'Destination is not a directory.'
743             return
744         
745         for item in self.current_list:
746             if len(item.notes)>0:
747                 try:
748                     shutil.copy(item.path, mydir)
749                 except OSError:
750                     print 'OSError. Cannot copy file. Perhaps you gave me a wrong directory?'
751
752 #OUTLET management
753
754
755     def do_outlet_show(self,args):
756         '''OUTLET_SHOW
757         ---------
758         Shows current content of outlet with index for reference
759         '''
760         self.outlet.printbuf()
761
762     def do_outlet_undo(self, args):
763         '''OUTLET_UNDO
764         ---------
765         Eliminates last entry in outlet
766         '''
767         print 'Erasing last entry'
768         self.outlet.pop()
769
770     def do_outlet_delete(self, args):
771         '''OUTLET_DELETE
772         Eliminates a particular entry from outlet
773         Syntax: outlet_delete n
774         '''
775         if len(args)==0:
776             print 'Index needed!, use outlet_show to know it'
777         else:
778             self.outlet.delete(args)
779
780
781
782
783
784 #OS INTERACTION COMMANDS
785 #-----------------    
786     def help_dir(self):
787         print '''
788 DIR, LS
789 Lists the files in the directory
790 ---------
791 Syntax: dir [path]
792           ls  [path]
793         '''
794     def do_dir(self,args):
795         
796         if len(args)==0:
797             args='*'
798         print glob.glob(args)
799         
800     def help_ls(self):
801         self.help_dir(self)
802     def do_ls(self,args):
803         self.do_dir(args)
804         
805     def help_pwd(self):
806         print '''
807 PWD
808 Gives the current working directory.
809 ------------
810 Syntax: pwd
811         '''
812     def do_pwd(self,args):
813         print os.getcwd()         
814     
815     def help_cd(self):
816         print '''
817 CD
818 Changes the current working directory
819 -----
820 Syntax: cd
821         '''
822     def do_cd(self,args):
823         mypath=os.path.abspath(args)
824         try:
825             os.chdir(mypath)
826         except OSError:
827             print 'I cannot access that directory.'
828     
829     
830     def help_system(self):
831         print '''
832 SYSTEM
833 Executes a system command line and reports the output
834 -----
835 Syntax system [command line]
836         '''
837         pass
838     def do_system(self,args):
839         waste=os.system(args)           
840             
841     def do_debug(self,args):
842         '''
843         this is a dummy command where I put debugging things
844         '''
845         print self.config['plotmanips']
846         pass
847             
848     def help_current(self):
849         print '''
850 CURRENT
851 Prints the current curve path.
852 ------
853 Syntax: current
854         '''
855     def do_current(self,args):
856         print self.current.path
857         
858     def do_version(self,args):
859         '''
860         VERSION
861         ------
862         Prints the current version and codename, plus library version. Useful for debugging.
863         '''     
864         print 'Hooke '+__version__+' ('+__codename__+')'
865         print 'Released on: '+__releasedate__
866         print '---'
867         print 'Python version: '+python_version
868         print 'WxPython version: '+wx_version
869         print 'wxMPL version: '+wxmpl_version
870         print 'Matplotlib version: '+mpl_version
871         print 'SciPy version: '+scipy_version
872         print 'NumPy version: '+numpy_version
873         print '---'
874         print 'Platform: '+str(platform.uname())
875         print '---'
876         print 'Loaded plugins:',self.config['loaded_plugins']
877         
878     def help_exit(self):
879         print '''
880 EXIT, QUIT
881 Exits the program cleanly.
882 ------
883 Syntax: exit
884 Syntax: quit
885 '''    
886     def do_exit(self,args):
887         we_exit='N'
888         
889         if (not self.playlist_saved) or (not self.notes_saved):
890             we_exit=linp.alphainput('You did not save your playlist and/or notes. Exit?','n',0,[])
891         else:
892             we_exit=linp.alphainput('Exit?','y',0,[])
893         
894         if we_exit[0].upper()=='Y':
895             wx.CallAfter(self.frame.Close)
896             sys.exit(0)
897         else:
898             return
899     
900     def help_quit(self):
901         self.help_exit()
902     def do_quit(self,args):
903         self.do_exit(args)
904
905
906
907
908
909 if __name__ == '__main__':
910     mycli=HookeCli(0)
911     mycli.cmdloop()