blob: 675d0cdad970a777f2d9ae5f864ed13ee32d4ad9 [file] [log] [blame]
carlosmscabralf40ecd12013-02-01 18:15:58 -02001#!/usr/bin/python
2
3"""
4MiniCCNxEdit: a simple network editor for MiniCCNx
5
6Based on miniedit by Bob Lantz, April 2010
7
8Carlos Cabral Jan, 2013
9
10
11"""
12import optparse
13
14from Tkinter import Frame, Button, Label, Scrollbar, Canvas
15from Tkinter import Menu, BitmapImage, PhotoImage, Wm, Toplevel
16
17# someday: from ttk import *
18
19from mininet.log import setLogLevel
20from mininet.net import Mininet
21from mininet.util import ipStr
22from mininet.term import makeTerm, cleanUpScreens
23
24
25def parse_args():
26 usage="""Usage: miniccnxedit [template_file]
27 If no template_file is given, generated template will be written
28 to the file miniccnx.conf in the current directory.
29 """
30
31 parser = optparse.OptionParser(usage)
32
33 _, arg = parser.parse_args()
34
35 if len(arg) != 1:
36 arg = None
37 else:
38 arg = arg[0]
39
40 return arg
41
42class MiniEdit( Frame ):
43
44 "A simple network editor for Mininet."
45
46 def __init__( self, parent=None, cheight=200, cwidth=500, template_file='miniccnx.conf' ):
47
48 Frame.__init__( self, parent )
49 self.action = None
50 self.appName = 'Mini-CCNx'
51 if template_file == None:
52 self.template_file='miniccnx.conf'
53 else:
54 self.template_file = template_file
55
56 # Style
57 self.font = ( 'Geneva', 9 )
58 self.smallFont = ( 'Geneva', 7 )
59 self.bg = 'white'
60
61 # Title
62 self.top = self.winfo_toplevel()
63 self.top.title( self.appName )
64
65 # Menu bar
66 self.createMenubar()
67
68 # Editing canvas
69 self.cheight, self.cwidth = cheight, cwidth
70 self.cframe, self.canvas = self.createCanvas()
71
72 # Toolbar
73 self.images = miniEditImages()
74 self.buttons = {}
75 self.active = None
76 self.tools = ( 'Select', 'Host', 'Switch', 'Link' )
77 self.customColors = { 'Switch': 'darkGreen', 'Host': 'blue' }
78 self.toolbar = self.createToolbar()
79
80 # Layout
81 self.toolbar.grid( column=0, row=0, sticky='nsew')
82 self.cframe.grid( column=1, row=0 )
83 self.columnconfigure( 1, weight=1 )
84 self.rowconfigure( 0, weight=1 )
85 self.pack( expand=True, fill='both' )
86
87 # About box
88 self.aboutBox = None
89
90 # Initialize node data
91 self.nodeBindings = self.createNodeBindings()
92 self.nodePrefixes = { 'Switch': 's', 'Host': 'h' }
93 self.widgetToItem = {}
94 self.itemToWidget = {}
95
96 # Initialize link tool
97 self.link = self.linkWidget = None
98
99 # Selection support
100 self.selection = None
101
102 # Keyboard bindings
103 self.bind( '<Control-q>', lambda event: self.quit() )
104 self.bind( '<KeyPress-Delete>', self.deleteSelection )
105 self.bind( '<KeyPress-BackSpace>', self.deleteSelection )
106 self.focus()
107
108 # Event handling initalization
109 self.linkx = self.linky = self.linkItem = None
110 self.lastSelection = None
111
112 # Model initialization
113 self.links = {}
114 self.nodeCount = 0
115 self.net = None
116
117 # Close window gracefully
118 Wm.wm_protocol( self.top, name='WM_DELETE_WINDOW', func=self.quit )
119
120 def quit( self ):
121 "Stop our network, if any, then quit."
122 self.stop()
123 Frame.quit( self )
124
125 def createMenubar( self ):
126 "Create our menu bar."
127
128 font = self.font
129
130 mbar = Menu( self.top, font=font )
131 self.top.configure( menu=mbar )
132
133 # Application menu
134 appMenu = Menu( mbar, tearoff=False )
135 mbar.add_cascade( label=self.appName, font=font, menu=appMenu )
136 appMenu.add_command( label='About Mini-CCNx', command=self.about,
137 font=font)
138 appMenu.add_separator()
139 appMenu.add_command( label='Quit', command=self.quit, font=font )
140
141 #fileMenu = Menu( mbar, tearoff=False )
142 #mbar.add_cascade( label="File", font=font, menu=fileMenu )
143 #fileMenu.add_command( label="Load...", font=font )
144 #fileMenu.add_separator()
145 #fileMenu.add_command( label="Save", font=font )
146 #fileMenu.add_separator()
147 #fileMenu.add_command( label="Print", font=font )
148
149 editMenu = Menu( mbar, tearoff=False )
150 mbar.add_cascade( label="Edit", font=font, menu=editMenu )
151 editMenu.add_command( label="Cut", font=font,
152 command=lambda: self.deleteSelection( None ) )
153
154 # runMenu = Menu( mbar, tearoff=False )
155 # mbar.add_cascade( label="Run", font=font, menu=runMenu )
156 # runMenu.add_command( label="Run", font=font, command=self.doRun )
157 # runMenu.add_command( label="Stop", font=font, command=self.doStop )
158 # runMenu.add_separator()
159 # runMenu.add_command( label='Xterm', font=font, command=self.xterm )
160
161 # Canvas
162
163 def createCanvas( self ):
164 "Create and return our scrolling canvas frame."
165 f = Frame( self )
166
167 canvas = Canvas( f, width=self.cwidth, height=self.cheight,
168 bg=self.bg )
169
170 # Scroll bars
171 xbar = Scrollbar( f, orient='horizontal', command=canvas.xview )
172 ybar = Scrollbar( f, orient='vertical', command=canvas.yview )
173 canvas.configure( xscrollcommand=xbar.set, yscrollcommand=ybar.set )
174
175 # Resize box
176 resize = Label( f, bg='white' )
177
178 # Layout
179 canvas.grid( row=0, column=1, sticky='nsew')
180 ybar.grid( row=0, column=2, sticky='ns')
181 xbar.grid( row=1, column=1, sticky='ew' )
182 resize.grid( row=1, column=2, sticky='nsew' )
183
184 # Resize behavior
185 f.rowconfigure( 0, weight=1 )
186 f.columnconfigure( 1, weight=1 )
187 f.grid( row=0, column=0, sticky='nsew' )
188 f.bind( '<Configure>', lambda event: self.updateScrollRegion() )
189
190 # Mouse bindings
191 canvas.bind( '<ButtonPress-1>', self.clickCanvas )
192 canvas.bind( '<B1-Motion>', self.dragCanvas )
193 canvas.bind( '<ButtonRelease-1>', self.releaseCanvas )
194
195 return f, canvas
196
197 def updateScrollRegion( self ):
198 "Update canvas scroll region to hold everything."
199 bbox = self.canvas.bbox( 'all' )
200 if bbox is not None:
201 self.canvas.configure( scrollregion=( 0, 0, bbox[ 2 ],
202 bbox[ 3 ] ) )
203
204 def canvasx( self, x_root ):
205 "Convert root x coordinate to canvas coordinate."
206 c = self.canvas
207 return c.canvasx( x_root ) - c.winfo_rootx()
208
209 def canvasy( self, y_root ):
210 "Convert root y coordinate to canvas coordinate."
211 c = self.canvas
212 return c.canvasy( y_root ) - c.winfo_rooty()
213
214 # Toolbar
215
216 def activate( self, toolName ):
217 "Activate a tool and press its button."
218 # Adjust button appearance
219 if self.active:
220 self.buttons[ self.active ].configure( relief='raised' )
221 self.buttons[ toolName ].configure( relief='sunken' )
222 # Activate dynamic bindings
223 self.active = toolName
224
225 def createToolbar( self ):
226 "Create and return our toolbar frame."
227
228 toolbar = Frame( self )
229
230 # Tools
231 for tool in self.tools:
232 cmd = ( lambda t=tool: self.activate( t ) )
233 b = Button( toolbar, text=tool, font=self.smallFont, command=cmd)
234 if tool in self.images:
235 b.config( height=50, image=self.images[ tool ] )
236 # b.config( compound='top' )
237 b.pack( fill='x' )
238 self.buttons[ tool ] = b
239 self.activate( self.tools[ 0 ] )
240
241 # Spacer
242 Label( toolbar, text='' ).pack()
243
244 # Commands
245 #for cmd, color in [ ( 'Stop', 'darkRed' ), ( 'Run', 'darkGreen' ) ]:
246 # doCmd = getattr( self, 'do' + cmd )
247 # b = Button( toolbar, text=cmd, font=self.smallFont,
248 # fg=color, command=doCmd )
249 # b.pack( fill='x', side='bottom' )
250
251 for cmd, color in [ ( 'Generate', 'darkGreen' ) ]:
252 doCmd = getattr( self, 'do' + cmd )
253 b = Button( toolbar, text=cmd, font=self.smallFont,
254 fg=color, command=doCmd )
255 b.pack( fill='x', side='bottom' )
256
257
258 return toolbar
259
260 def doGenerate( self ):
261 "Generate template."
262 self.activate( 'Select' )
263 for tool in self.tools:
264 self.buttons[ tool ].config( state='disabled' )
265
266 self.buildTemplate()
267
268 for tool in self.tools:
269 self.buttons[ tool ].config( state='normal' )
270
271 def doStop( self ):
272 "Stop command."
273 self.stop()
274 for tool in self.tools:
275 self.buttons[ tool ].config( state='normal' )
276
277 def buildTemplate( self ):
278 "Generate template"
279
280 template = open(self.template_file, 'w')
281
282 # hosts
283 template.write('[hosts]\n')
284 for widget in self.widgetToItem:
285 name = widget[ 'text' ]
286 tags = self.canvas.gettags( self.widgetToItem[ widget ] )
287 if 'Host' in tags:
288 template.write(name + ':\n')
289
290 # switches/routers
291 template.write('[routers]\n')
292 for widget in self.widgetToItem:
293 name = widget[ 'text' ]
294 tags = self.canvas.gettags( self.widgetToItem[ widget ] )
295 if 'Switch' in tags:
296 template.write(name + ':\n')
297
298 # Make links
299 template.write('[links]\n')
300 for link in self.links.values():
301 ( src, dst ) = link
302 srcName, dstName = src[ 'text' ], dst[ 'text' ]
303 template.write(srcName + ':' + dstName + '\n')
304
305 template.close()
306
307
308 # Generic canvas handler
309 #
310 # We could have used bindtags, as in nodeIcon, but
311 # the dynamic approach used here
312 # may actually require less code. In any case, it's an
313 # interesting introspection-based alternative to bindtags.
314
315 def canvasHandle( self, eventName, event ):
316 "Generic canvas event handler"
317 if self.active is None:
318 return
319 toolName = self.active
320 handler = getattr( self, eventName + toolName, None )
321 if handler is not None:
322 handler( event )
323
324 def clickCanvas( self, event ):
325 "Canvas click handler."
326 self.canvasHandle( 'click', event )
327
328 def dragCanvas( self, event ):
329 "Canvas drag handler."
330 self.canvasHandle( 'drag', event )
331
332 def releaseCanvas( self, event ):
333 "Canvas mouse up handler."
334 self.canvasHandle( 'release', event )
335
336 # Currently the only items we can select directly are
337 # links. Nodes are handled by bindings in the node icon.
338
339 def findItem( self, x, y ):
340 "Find items at a location in our canvas."
341 items = self.canvas.find_overlapping( x, y, x, y )
342 if len( items ) == 0:
343 return None
344 else:
345 return items[ 0 ]
346
347 # Canvas bindings for Select, Host, Switch and Link tools
348
349 def clickSelect( self, event ):
350 "Select an item."
351 self.selectItem( self.findItem( event.x, event.y ) )
352
353 def deleteItem( self, item ):
354 "Delete an item."
355 # Don't delete while network is running
356 if self.buttons[ 'Select' ][ 'state' ] == 'disabled':
357 return
358 # Delete from model
359 if item in self.links:
360 self.deleteLink( item )
361 if item in self.itemToWidget:
362 self.deleteNode( item )
363 # Delete from view
364 self.canvas.delete( item )
365
366 def deleteSelection( self, _event ):
367 "Delete the selected item."
368 if self.selection is not None:
369 self.deleteItem( self.selection )
370 self.selectItem( None )
371
372 def nodeIcon( self, node, name ):
373 "Create a new node icon."
374 icon = Button( self.canvas, image=self.images[ node ],
375 text=name, compound='top' )
376 # Unfortunately bindtags wants a tuple
377 bindtags = [ str( self.nodeBindings ) ]
378 bindtags += list( icon.bindtags() )
379 icon.bindtags( tuple( bindtags ) )
380 return icon
381
382 def newNode( self, node, event ):
383 "Add a new node to our canvas."
384 c = self.canvas
385 x, y = c.canvasx( event.x ), c.canvasy( event.y )
386 self.nodeCount += 1
387 name = self.nodePrefixes[ node ] + str( self.nodeCount )
388 icon = self.nodeIcon( node, name )
389 item = self.canvas.create_window( x, y, anchor='c', window=icon,
390 tags=node )
391 self.widgetToItem[ icon ] = item
392 self.itemToWidget[ item ] = icon
393 self.selectItem( item )
394 icon.links = {}
395
396 def clickHost( self, event ):
397 "Add a new host to our canvas."
398 self.newNode( 'Host', event )
399
400 def clickSwitch( self, event ):
401 "Add a new switch to our canvas."
402 self.newNode( 'Switch', event )
403
404 def dragLink( self, event ):
405 "Drag a link's endpoint to another node."
406 if self.link is None:
407 return
408 # Since drag starts in widget, we use root coords
409 x = self.canvasx( event.x_root )
410 y = self.canvasy( event.y_root )
411 c = self.canvas
412 c.coords( self.link, self.linkx, self.linky, x, y )
413
414 def releaseLink( self, _event ):
415 "Give up on the current link."
416 if self.link is not None:
417 self.canvas.delete( self.link )
418 self.linkWidget = self.linkItem = self.link = None
419
420 # Generic node handlers
421
422 def createNodeBindings( self ):
423 "Create a set of bindings for nodes."
424 bindings = {
425 '<ButtonPress-1>': self.clickNode,
426 '<B1-Motion>': self.dragNode,
427 '<ButtonRelease-1>': self.releaseNode,
428 '<Enter>': self.enterNode,
429 '<Leave>': self.leaveNode,
430 '<Double-ButtonPress-1>': self.xterm
431 }
432 l = Label() # lightweight-ish owner for bindings
433 for event, binding in bindings.items():
434 l.bind( event, binding )
435 return l
436
437 def selectItem( self, item ):
438 "Select an item and remember old selection."
439 self.lastSelection = self.selection
440 self.selection = item
441
442 def enterNode( self, event ):
443 "Select node on entry."
444 self.selectNode( event )
445
446 def leaveNode( self, _event ):
447 "Restore old selection on exit."
448 self.selectItem( self.lastSelection )
449
450 def clickNode( self, event ):
451 "Node click handler."
452 if self.active is 'Link':
453 self.startLink( event )
454 else:
455 self.selectNode( event )
456 return 'break'
457
458 def dragNode( self, event ):
459 "Node drag handler."
460 if self.active is 'Link':
461 self.dragLink( event )
462 else:
463 self.dragNodeAround( event )
464
465 def releaseNode( self, event ):
466 "Node release handler."
467 if self.active is 'Link':
468 self.finishLink( event )
469
470 # Specific node handlers
471
472 def selectNode( self, event ):
473 "Select the node that was clicked on."
474 item = self.widgetToItem.get( event.widget, None )
475 self.selectItem( item )
476
477 def dragNodeAround( self, event ):
478 "Drag a node around on the canvas."
479 c = self.canvas
480 # Convert global to local coordinates;
481 # Necessary since x, y are widget-relative
482 x = self.canvasx( event.x_root )
483 y = self.canvasy( event.y_root )
484 w = event.widget
485 # Adjust node position
486 item = self.widgetToItem[ w ]
487 c.coords( item, x, y )
488 # Adjust link positions
489 for dest in w.links:
490 link = w.links[ dest ]
491 item = self.widgetToItem[ dest ]
492 x1, y1 = c.coords( item )
493 c.coords( link, x, y, x1, y1 )
494
495 def startLink( self, event ):
496 "Start a new link."
497 if event.widget not in self.widgetToItem:
498 # Didn't click on a node
499 return
500 w = event.widget
501 item = self.widgetToItem[ w ]
502 x, y = self.canvas.coords( item )
503 self.link = self.canvas.create_line( x, y, x, y, width=4,
504 fill='blue', tag='link' )
505 self.linkx, self.linky = x, y
506 self.linkWidget = w
507 self.linkItem = item
508
509 # Link bindings
510 # Selection still needs a bit of work overall
511 # Callbacks ignore event
512
513 def select( _event, link=self.link ):
514 "Select item on mouse entry."
515 self.selectItem( link )
516
517 def highlight( _event, link=self.link ):
518 "Highlight item on mouse entry."
519 # self.selectItem( link )
520 self.canvas.itemconfig( link, fill='green' )
521
522 def unhighlight( _event, link=self.link ):
523 "Unhighlight item on mouse exit."
524 self.canvas.itemconfig( link, fill='blue' )
525 # self.selectItem( None )
526
527 self.canvas.tag_bind( self.link, '<Enter>', highlight )
528 self.canvas.tag_bind( self.link, '<Leave>', unhighlight )
529 self.canvas.tag_bind( self.link, '<ButtonPress-1>', select )
530
531 def finishLink( self, event ):
532 "Finish creating a link"
533 if self.link is None:
534 return
535 source = self.linkWidget
536 c = self.canvas
537 # Since we dragged from the widget, use root coords
538 x, y = self.canvasx( event.x_root ), self.canvasy( event.y_root )
539 target = self.findItem( x, y )
540 dest = self.itemToWidget.get( target, None )
541 if ( source is None or dest is None or source == dest
542 or dest in source.links or source in dest.links ):
543 self.releaseLink( event )
544 return
545 # For now, don't allow hosts to be directly linked
546# stags = self.canvas.gettags( self.widgetToItem[ source ] )
547# dtags = self.canvas.gettags( target )
548# if 'Host' in stags and 'Host' in dtags:
549# self.releaseLink( event )
550# return
551 x, y = c.coords( target )
552 c.coords( self.link, self.linkx, self.linky, x, y )
553 self.addLink( source, dest )
554 # We're done
555 self.link = self.linkWidget = None
556
557 # Menu handlers
558
559 def about( self ):
560 "Display about box."
561 about = self.aboutBox
562 if about is None:
563 bg = 'white'
564 about = Toplevel( bg='white' )
565 about.title( 'About' )
566 info = self.appName + ': a simple network editor for Mini-CCNx - based on Miniedit '
567 warning = 'Development version - not entirely functional!'
568 author = 'Carlos Cabral, Jan 2013'
569 author2 = 'Miniedit by Bob Lantz <rlantz@cs>, April 2010'
570 line1 = Label( about, text=info, font='Helvetica 10 bold', bg=bg )
571 line2 = Label( about, text=warning, font='Helvetica 9', bg=bg )
572 line3 = Label( about, text=author, font='Helvetica 9', bg=bg )
573 line4 = Label( about, text=author2, font='Helvetica 9', bg=bg )
574 line1.pack( padx=20, pady=10 )
575 line2.pack(pady=10 )
576 line3.pack(pady=10 )
577 line4.pack(pady=10 )
578 hide = ( lambda about=about: about.withdraw() )
579 self.aboutBox = about
580 # Hide on close rather than destroying window
581 Wm.wm_protocol( about, name='WM_DELETE_WINDOW', func=hide )
582 # Show (existing) window
583 about.deiconify()
584
585 def createToolImages( self ):
586 "Create toolbar (and icon) images."
587
588 # Model interface
589 #
590 # Ultimately we will either want to use a topo or
591 # mininet object here, probably.
592
593 def addLink( self, source, dest ):
594 "Add link to model."
595 source.links[ dest ] = self.link
596 dest.links[ source ] = self.link
597 self.links[ self.link ] = ( source, dest )
598
599 def deleteLink( self, link ):
600 "Delete link from model."
601 pair = self.links.get( link, None )
602 if pair is not None:
603 source, dest = pair
604 del source.links[ dest ]
605 del dest.links[ source ]
606 if link is not None:
607 del self.links[ link ]
608
609 def deleteNode( self, item ):
610 "Delete node (and its links) from model."
611 widget = self.itemToWidget[ item ]
612 for link in widget.links.values():
613 # Delete from view and model
614 self.deleteItem( link )
615 del self.itemToWidget[ item ]
616 del self.widgetToItem[ widget ]
617
618 def build( self ):
619 "Build network based on our topology."
620
621 net = Mininet( topo=None )
622
623 # Make controller
624 net.addController( 'c0' )
625 # Make nodes
626 for widget in self.widgetToItem:
627 name = widget[ 'text' ]
628 tags = self.canvas.gettags( self.widgetToItem[ widget ] )
629 nodeNum = int( name[ 1: ] )
630 if 'Switch' in tags:
631 net.addSwitch( name )
632 elif 'Host' in tags:
633 net.addHost( name, ip=ipStr( nodeNum ) )
634 else:
635 raise Exception( "Cannot create mystery node: " + name )
636 # Make links
637 for link in self.links.values():
638 ( src, dst ) = link
639 srcName, dstName = src[ 'text' ], dst[ 'text' ]
640 src, dst = net.nameToNode[ srcName ], net.nameToNode[ dstName ]
641 src.linkTo( dst )
642
643 # Build network (we have to do this separately at the moment )
644 net.build()
645
646 return net
647
648 def start( self ):
649 "Start network."
650 if self.net is None:
651 self.net = self.build()
652 self.net.start()
653
654 def stop( self ):
655 "Stop network."
656 if self.net is not None:
657 self.net.stop()
658 cleanUpScreens()
659 self.net = None
660
661 def xterm( self, _ignore=None ):
662 "Make an xterm when a button is pressed."
663 if ( self.selection is None or
664 self.net is None or
665 self.selection not in self.itemToWidget ):
666 return
667 name = self.itemToWidget[ self.selection ][ 'text' ]
668 if name not in self.net.nameToNode:
669 return
670 term = makeTerm( self.net.nameToNode[ name ], 'Host' )
671 self.net.terms.append( term )
672
673
674def miniEditImages():
675 "Create and return images for MiniEdit."
676
677 # Image data. Git will be unhappy. However, the alternative
678 # is to keep track of separate binary files, which is also
679 # unappealing.
680
681 return {
682 'Select': BitmapImage(
683 file='/usr/include/X11/bitmaps/left_ptr' ),
684
685 'Switch' : PhotoImage( data=r"""
686 R0lGODlhOgArAMIEAB8aFwB7tQCb343L8P///////////////yH+GlNvZnR3YXJlOiBNaWNyb3NvZnQgT2ZmaWNlACwAAAAAOgArAAAD/ki63P4wykmrvTjr3YYfQigKH7d5Y6qmnjmBayyHg8vAAqDPaUTbowaA13OIahqcyEgEQEbIi7LIGA1FzsaSQK0QfbnH10sMa83VsqX53HLL7sgUTudR5s367F7PEq4CYDJRcngqfgshiAqAMwF3AYdWTCERjSoBjy+ZVItvMg6XIZmaEgOkSmJwlKOkkKSRlaqraaewr7ABhnqBNLmZuL+6vCzCrpvGsB9EH8m5wc7R0sbQ09bT1dOEBLbXwMjeEN7HpuO6Dt3hFObi7Ovj7d7bEOnYD+4V8PfqF/wN/lKsxZPmop6wBwaFzTsRbVvCWzYQmlMW0UKzZCUqatzICLGjx48gKyYAADs=
687"""),
688 'Host' : PhotoImage( data=r"""
689 R0lGODlhKQAyAMIHAJyeoK+wsrW2uMHCxM7P0Ozt7fn5+f///yH+EUNyZWF0ZWQgd2l0aCBHSU1QACwAAAAAKQAyAAAD63i63P4wykmrvS4cwLv/IEhxRxGeKGBM3pa+X9QeBmxrT3gMNpyLrt6rgcJgisKXgIFMopaLpjMEVUinn2pQ1ImSrN8uGKCVegHn8bl8CqbV7jFbJ47H650592sX4zl6MX9ocIOBLYNvhkxtiYV8eYx0kJSEi2d7WFmSmZqRmIKeHoddoqOcoaZkqIiqq6CtqqQkrq9jnaKzaLW6Wy8DBMHCp7ClPT+ArMY2t1u9Qs3Et6k+W87KtMfW0r6x1d7P2uDYu+LLtt3nQ9ufxeXM7MkOuCnR7UTe6/jyEOqeWj/SYQEowxXBfgYPJAAAOw==
690"""),
691 'Link': PhotoImage( data=r"""
692 R0lGODlhFgAWAPcAMf//////zP//mf//Zv//M///AP/M///MzP/M
693 mf/MZv/MM//MAP+Z//+ZzP+Zmf+ZZv+ZM/+ZAP9m//9mzP9mmf9m
694 Zv9mM/9mAP8z//8zzP8zmf8zZv8zM/8zAP8A//8AzP8Amf8AZv8A
695 M/8AAMz//8z/zMz/mcz/Zsz/M8z/AMzM/8zMzMzMmczMZszMM8zM
696 AMyZ/8yZzMyZmcyZZsyZM8yZAMxm/8xmzMxmmcxmZsxmM8xmAMwz
697 /8wzzMwzmcwzZswzM8wzAMwA/8wAzMwAmcwAZswAM8wAAJn//5n/
698 zJn/mZn/Zpn/M5n/AJnM/5nMzJnMmZnMZpnMM5nMAJmZ/5mZzJmZ
699 mZmZZpmZM5mZAJlm/5lmzJlmmZlmZplmM5lmAJkz/5kzzJkzmZkz
700 ZpkzM5kzAJkA/5kAzJkAmZkAZpkAM5kAAGb//2b/zGb/mWb/Zmb/
701 M2b/AGbM/2bMzGbMmWbMZmbMM2bMAGaZ/2aZzGaZmWaZZmaZM2aZ
702 AGZm/2ZmzGZmmWZmZmZmM2ZmAGYz/2YzzGYzmWYzZmYzM2YzAGYA
703 /2YAzGYAmWYAZmYAM2YAADP//zP/zDP/mTP/ZjP/MzP/ADPM/zPM
704 zDPMmTPMZjPMMzPMADOZ/zOZzDOZmTOZZjOZMzOZADNm/zNmzDNm
705 mTNmZjNmMzNmADMz/zMzzDMzmTMzZjMzMzMzADMA/zMAzDMAmTMA
706 ZjMAMzMAAAD//wD/zAD/mQD/ZgD/MwD/AADM/wDMzADMmQDMZgDM
707 MwDMAACZ/wCZzACZmQCZZgCZMwCZAABm/wBmzABmmQBmZgBmMwBm
708 AAAz/wAzzAAzmQAzZgAzMwAzAAAA/wAAzAAAmQAAZgAAM+4AAN0A
709 ALsAAKoAAIgAAHcAAFUAAEQAACIAABEAAADuAADdAAC7AACqAACI
710 AAB3AABVAABEAAAiAAARAAAA7gAA3QAAuwAAqgAAiAAAdwAAVQAA
711 RAAAIgAAEe7u7t3d3bu7u6qqqoiIiHd3d1VVVURERCIiIhEREQAA
712 ACH5BAEAAAAALAAAAAAWABYAAAhIAAEIHEiwoEGBrhIeXEgwoUKG
713 Cx0+hGhQoiuKBy1irChxY0GNHgeCDAlgZEiTHlFuVImRJUWXEGEy
714 lBmxI8mSNknm1Dnx5sCAADs=
715 """ )
716 }
717
718if __name__ == '__main__':
719 setLogLevel( 'info' )
720 temp_file = parse_args()
721 app = MiniEdit(template_file=temp_file)
722 app.mainloop()