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