| #!/usr/bin/python |
| |
| """ |
| MiniCCNxEdit: a simple network editor for MiniCCNx |
| |
| Based on miniedit by Bob Lantz, April 2010 |
| |
| Carlos Cabral Jan, 2013 |
| |
| |
| """ |
| import optparse |
| |
| from Tkinter import Frame, Button, Label, Scrollbar, Canvas |
| from Tkinter import Menu, BitmapImage, PhotoImage, Wm, Toplevel |
| |
| # someday: from ttk import * |
| |
| from mininet.log import setLogLevel |
| from mininet.net import Mininet |
| from mininet.util import ipStr |
| from mininet.term import makeTerm, cleanUpScreens |
| |
| |
| def parse_args(): |
| usage="""Usage: miniccnxedit [template_file] |
| If no template_file is given, generated template will be written |
| to the file miniccnx.conf in the current directory. |
| """ |
| |
| parser = optparse.OptionParser(usage) |
| |
| _, arg = parser.parse_args() |
| |
| if len(arg) != 1: |
| arg = None |
| else: |
| arg = arg[0] |
| |
| return arg |
| |
| class MiniEdit( Frame ): |
| |
| "A simple network editor for Mininet." |
| |
| def __init__( self, parent=None, cheight=200, cwidth=500, template_file='miniccnx.conf' ): |
| |
| Frame.__init__( self, parent ) |
| self.action = None |
| self.appName = 'Mini-CCNx' |
| if template_file == None: |
| self.template_file='miniccnx.conf' |
| else: |
| self.template_file = template_file |
| |
| # Style |
| self.font = ( 'Geneva', 9 ) |
| self.smallFont = ( 'Geneva', 7 ) |
| self.bg = 'white' |
| |
| # Title |
| self.top = self.winfo_toplevel() |
| self.top.title( self.appName ) |
| |
| # Menu bar |
| self.createMenubar() |
| |
| # Editing canvas |
| self.cheight, self.cwidth = cheight, cwidth |
| self.cframe, self.canvas = self.createCanvas() |
| |
| # Toolbar |
| self.images = miniEditImages() |
| self.buttons = {} |
| self.active = None |
| self.tools = ( 'Select', 'Host', 'Switch', 'Link' ) |
| self.customColors = { 'Switch': 'darkGreen', 'Host': 'blue' } |
| self.toolbar = self.createToolbar() |
| |
| # Layout |
| self.toolbar.grid( column=0, row=0, sticky='nsew') |
| self.cframe.grid( column=1, row=0 ) |
| self.columnconfigure( 1, weight=1 ) |
| self.rowconfigure( 0, weight=1 ) |
| self.pack( expand=True, fill='both' ) |
| |
| # About box |
| self.aboutBox = None |
| |
| # Initialize node data |
| self.nodeBindings = self.createNodeBindings() |
| self.nodePrefixes = { 'Switch': 's', 'Host': 'h' } |
| self.widgetToItem = {} |
| self.itemToWidget = {} |
| |
| # Initialize link tool |
| self.link = self.linkWidget = None |
| |
| # Selection support |
| self.selection = None |
| |
| # Keyboard bindings |
| self.bind( '<Control-q>', lambda event: self.quit() ) |
| self.bind( '<KeyPress-Delete>', self.deleteSelection ) |
| self.bind( '<KeyPress-BackSpace>', self.deleteSelection ) |
| self.focus() |
| |
| # Event handling initalization |
| self.linkx = self.linky = self.linkItem = None |
| self.lastSelection = None |
| |
| # Model initialization |
| self.links = {} |
| self.nodeCount = 0 |
| self.net = None |
| |
| # Close window gracefully |
| Wm.wm_protocol( self.top, name='WM_DELETE_WINDOW', func=self.quit ) |
| |
| def quit( self ): |
| "Stop our network, if any, then quit." |
| self.stop() |
| Frame.quit( self ) |
| |
| def createMenubar( self ): |
| "Create our menu bar." |
| |
| font = self.font |
| |
| mbar = Menu( self.top, font=font ) |
| self.top.configure( menu=mbar ) |
| |
| # Application menu |
| appMenu = Menu( mbar, tearoff=False ) |
| mbar.add_cascade( label=self.appName, font=font, menu=appMenu ) |
| appMenu.add_command( label='About Mini-CCNx', command=self.about, |
| font=font) |
| appMenu.add_separator() |
| appMenu.add_command( label='Quit', command=self.quit, font=font ) |
| |
| #fileMenu = Menu( mbar, tearoff=False ) |
| #mbar.add_cascade( label="File", font=font, menu=fileMenu ) |
| #fileMenu.add_command( label="Load...", font=font ) |
| #fileMenu.add_separator() |
| #fileMenu.add_command( label="Save", font=font ) |
| #fileMenu.add_separator() |
| #fileMenu.add_command( label="Print", font=font ) |
| |
| editMenu = Menu( mbar, tearoff=False ) |
| mbar.add_cascade( label="Edit", font=font, menu=editMenu ) |
| editMenu.add_command( label="Cut", font=font, |
| command=lambda: self.deleteSelection( None ) ) |
| |
| # runMenu = Menu( mbar, tearoff=False ) |
| # mbar.add_cascade( label="Run", font=font, menu=runMenu ) |
| # runMenu.add_command( label="Run", font=font, command=self.doRun ) |
| # runMenu.add_command( label="Stop", font=font, command=self.doStop ) |
| # runMenu.add_separator() |
| # runMenu.add_command( label='Xterm', font=font, command=self.xterm ) |
| |
| # Canvas |
| |
| def createCanvas( self ): |
| "Create and return our scrolling canvas frame." |
| f = Frame( self ) |
| |
| canvas = Canvas( f, width=self.cwidth, height=self.cheight, |
| bg=self.bg ) |
| |
| # Scroll bars |
| xbar = Scrollbar( f, orient='horizontal', command=canvas.xview ) |
| ybar = Scrollbar( f, orient='vertical', command=canvas.yview ) |
| canvas.configure( xscrollcommand=xbar.set, yscrollcommand=ybar.set ) |
| |
| # Resize box |
| resize = Label( f, bg='white' ) |
| |
| # Layout |
| canvas.grid( row=0, column=1, sticky='nsew') |
| ybar.grid( row=0, column=2, sticky='ns') |
| xbar.grid( row=1, column=1, sticky='ew' ) |
| resize.grid( row=1, column=2, sticky='nsew' ) |
| |
| # Resize behavior |
| f.rowconfigure( 0, weight=1 ) |
| f.columnconfigure( 1, weight=1 ) |
| f.grid( row=0, column=0, sticky='nsew' ) |
| f.bind( '<Configure>', lambda event: self.updateScrollRegion() ) |
| |
| # Mouse bindings |
| canvas.bind( '<ButtonPress-1>', self.clickCanvas ) |
| canvas.bind( '<B1-Motion>', self.dragCanvas ) |
| canvas.bind( '<ButtonRelease-1>', self.releaseCanvas ) |
| |
| return f, canvas |
| |
| def updateScrollRegion( self ): |
| "Update canvas scroll region to hold everything." |
| bbox = self.canvas.bbox( 'all' ) |
| if bbox is not None: |
| self.canvas.configure( scrollregion=( 0, 0, bbox[ 2 ], |
| bbox[ 3 ] ) ) |
| |
| def canvasx( self, x_root ): |
| "Convert root x coordinate to canvas coordinate." |
| c = self.canvas |
| return c.canvasx( x_root ) - c.winfo_rootx() |
| |
| def canvasy( self, y_root ): |
| "Convert root y coordinate to canvas coordinate." |
| c = self.canvas |
| return c.canvasy( y_root ) - c.winfo_rooty() |
| |
| # Toolbar |
| |
| def activate( self, toolName ): |
| "Activate a tool and press its button." |
| # Adjust button appearance |
| if self.active: |
| self.buttons[ self.active ].configure( relief='raised' ) |
| self.buttons[ toolName ].configure( relief='sunken' ) |
| # Activate dynamic bindings |
| self.active = toolName |
| |
| def createToolbar( self ): |
| "Create and return our toolbar frame." |
| |
| toolbar = Frame( self ) |
| |
| # Tools |
| for tool in self.tools: |
| cmd = ( lambda t=tool: self.activate( t ) ) |
| b = Button( toolbar, text=tool, font=self.smallFont, command=cmd) |
| if tool in self.images: |
| b.config( height=50, image=self.images[ tool ] ) |
| # b.config( compound='top' ) |
| b.pack( fill='x' ) |
| self.buttons[ tool ] = b |
| self.activate( self.tools[ 0 ] ) |
| |
| # Spacer |
| Label( toolbar, text='' ).pack() |
| |
| # Commands |
| #for cmd, color in [ ( 'Stop', 'darkRed' ), ( 'Run', 'darkGreen' ) ]: |
| # doCmd = getattr( self, 'do' + cmd ) |
| # b = Button( toolbar, text=cmd, font=self.smallFont, |
| # fg=color, command=doCmd ) |
| # b.pack( fill='x', side='bottom' ) |
| |
| for cmd, color in [ ( 'Generate', 'darkGreen' ) ]: |
| doCmd = getattr( self, 'do' + cmd ) |
| b = Button( toolbar, text=cmd, font=self.smallFont, |
| fg=color, command=doCmd ) |
| b.pack( fill='x', side='bottom' ) |
| |
| |
| return toolbar |
| |
| def doGenerate( self ): |
| "Generate template." |
| self.activate( 'Select' ) |
| for tool in self.tools: |
| self.buttons[ tool ].config( state='disabled' ) |
| |
| self.buildTemplate() |
| |
| for tool in self.tools: |
| self.buttons[ tool ].config( state='normal' ) |
| |
| def doStop( self ): |
| "Stop command." |
| self.stop() |
| for tool in self.tools: |
| self.buttons[ tool ].config( state='normal' ) |
| |
| def buildTemplate( self ): |
| "Generate template" |
| |
| template = open(self.template_file, 'w') |
| |
| # hosts |
| template.write('[hosts]\n') |
| for widget in self.widgetToItem: |
| name = widget[ 'text' ] |
| tags = self.canvas.gettags( self.widgetToItem[ widget ] ) |
| if 'Host' in tags: |
| template.write(name + ':\n') |
| |
| # switches/routers |
| template.write('[routers]\n') |
| for widget in self.widgetToItem: |
| name = widget[ 'text' ] |
| tags = self.canvas.gettags( self.widgetToItem[ widget ] ) |
| if 'Switch' in tags: |
| template.write(name + ':\n') |
| |
| # Make links |
| template.write('[links]\n') |
| for link in self.links.values(): |
| ( src, dst ) = link |
| srcName, dstName = src[ 'text' ], dst[ 'text' ] |
| template.write(srcName + ':' + dstName + '\n') |
| |
| template.close() |
| |
| |
| # Generic canvas handler |
| # |
| # We could have used bindtags, as in nodeIcon, but |
| # the dynamic approach used here |
| # may actually require less code. In any case, it's an |
| # interesting introspection-based alternative to bindtags. |
| |
| def canvasHandle( self, eventName, event ): |
| "Generic canvas event handler" |
| if self.active is None: |
| return |
| toolName = self.active |
| handler = getattr( self, eventName + toolName, None ) |
| if handler is not None: |
| handler( event ) |
| |
| def clickCanvas( self, event ): |
| "Canvas click handler." |
| self.canvasHandle( 'click', event ) |
| |
| def dragCanvas( self, event ): |
| "Canvas drag handler." |
| self.canvasHandle( 'drag', event ) |
| |
| def releaseCanvas( self, event ): |
| "Canvas mouse up handler." |
| self.canvasHandle( 'release', event ) |
| |
| # Currently the only items we can select directly are |
| # links. Nodes are handled by bindings in the node icon. |
| |
| def findItem( self, x, y ): |
| "Find items at a location in our canvas." |
| items = self.canvas.find_overlapping( x, y, x, y ) |
| if len( items ) == 0: |
| return None |
| else: |
| return items[ 0 ] |
| |
| # Canvas bindings for Select, Host, Switch and Link tools |
| |
| def clickSelect( self, event ): |
| "Select an item." |
| self.selectItem( self.findItem( event.x, event.y ) ) |
| |
| def deleteItem( self, item ): |
| "Delete an item." |
| # Don't delete while network is running |
| if self.buttons[ 'Select' ][ 'state' ] == 'disabled': |
| return |
| # Delete from model |
| if item in self.links: |
| self.deleteLink( item ) |
| if item in self.itemToWidget: |
| self.deleteNode( item ) |
| # Delete from view |
| self.canvas.delete( item ) |
| |
| def deleteSelection( self, _event ): |
| "Delete the selected item." |
| if self.selection is not None: |
| self.deleteItem( self.selection ) |
| self.selectItem( None ) |
| |
| def nodeIcon( self, node, name ): |
| "Create a new node icon." |
| icon = Button( self.canvas, image=self.images[ node ], |
| text=name, compound='top' ) |
| # Unfortunately bindtags wants a tuple |
| bindtags = [ str( self.nodeBindings ) ] |
| bindtags += list( icon.bindtags() ) |
| icon.bindtags( tuple( bindtags ) ) |
| return icon |
| |
| def newNode( self, node, event ): |
| "Add a new node to our canvas." |
| c = self.canvas |
| x, y = c.canvasx( event.x ), c.canvasy( event.y ) |
| self.nodeCount += 1 |
| name = self.nodePrefixes[ node ] + str( self.nodeCount ) |
| icon = self.nodeIcon( node, name ) |
| item = self.canvas.create_window( x, y, anchor='c', window=icon, |
| tags=node ) |
| self.widgetToItem[ icon ] = item |
| self.itemToWidget[ item ] = icon |
| self.selectItem( item ) |
| icon.links = {} |
| |
| def clickHost( self, event ): |
| "Add a new host to our canvas." |
| self.newNode( 'Host', event ) |
| |
| def clickSwitch( self, event ): |
| "Add a new switch to our canvas." |
| self.newNode( 'Switch', event ) |
| |
| def dragLink( self, event ): |
| "Drag a link's endpoint to another node." |
| if self.link is None: |
| return |
| # Since drag starts in widget, we use root coords |
| x = self.canvasx( event.x_root ) |
| y = self.canvasy( event.y_root ) |
| c = self.canvas |
| c.coords( self.link, self.linkx, self.linky, x, y ) |
| |
| def releaseLink( self, _event ): |
| "Give up on the current link." |
| if self.link is not None: |
| self.canvas.delete( self.link ) |
| self.linkWidget = self.linkItem = self.link = None |
| |
| # Generic node handlers |
| |
| def createNodeBindings( self ): |
| "Create a set of bindings for nodes." |
| bindings = { |
| '<ButtonPress-1>': self.clickNode, |
| '<B1-Motion>': self.dragNode, |
| '<ButtonRelease-1>': self.releaseNode, |
| '<Enter>': self.enterNode, |
| '<Leave>': self.leaveNode, |
| '<Double-ButtonPress-1>': self.xterm |
| } |
| l = Label() # lightweight-ish owner for bindings |
| for event, binding in bindings.items(): |
| l.bind( event, binding ) |
| return l |
| |
| def selectItem( self, item ): |
| "Select an item and remember old selection." |
| self.lastSelection = self.selection |
| self.selection = item |
| |
| def enterNode( self, event ): |
| "Select node on entry." |
| self.selectNode( event ) |
| |
| def leaveNode( self, _event ): |
| "Restore old selection on exit." |
| self.selectItem( self.lastSelection ) |
| |
| def clickNode( self, event ): |
| "Node click handler." |
| if self.active is 'Link': |
| self.startLink( event ) |
| else: |
| self.selectNode( event ) |
| return 'break' |
| |
| def dragNode( self, event ): |
| "Node drag handler." |
| if self.active is 'Link': |
| self.dragLink( event ) |
| else: |
| self.dragNodeAround( event ) |
| |
| def releaseNode( self, event ): |
| "Node release handler." |
| if self.active is 'Link': |
| self.finishLink( event ) |
| |
| # Specific node handlers |
| |
| def selectNode( self, event ): |
| "Select the node that was clicked on." |
| item = self.widgetToItem.get( event.widget, None ) |
| self.selectItem( item ) |
| |
| def dragNodeAround( self, event ): |
| "Drag a node around on the canvas." |
| c = self.canvas |
| # Convert global to local coordinates; |
| # Necessary since x, y are widget-relative |
| x = self.canvasx( event.x_root ) |
| y = self.canvasy( event.y_root ) |
| w = event.widget |
| # Adjust node position |
| item = self.widgetToItem[ w ] |
| c.coords( item, x, y ) |
| # Adjust link positions |
| for dest in w.links: |
| link = w.links[ dest ] |
| item = self.widgetToItem[ dest ] |
| x1, y1 = c.coords( item ) |
| c.coords( link, x, y, x1, y1 ) |
| |
| def startLink( self, event ): |
| "Start a new link." |
| if event.widget not in self.widgetToItem: |
| # Didn't click on a node |
| return |
| w = event.widget |
| item = self.widgetToItem[ w ] |
| x, y = self.canvas.coords( item ) |
| self.link = self.canvas.create_line( x, y, x, y, width=4, |
| fill='blue', tag='link' ) |
| self.linkx, self.linky = x, y |
| self.linkWidget = w |
| self.linkItem = item |
| |
| # Link bindings |
| # Selection still needs a bit of work overall |
| # Callbacks ignore event |
| |
| def select( _event, link=self.link ): |
| "Select item on mouse entry." |
| self.selectItem( link ) |
| |
| def highlight( _event, link=self.link ): |
| "Highlight item on mouse entry." |
| # self.selectItem( link ) |
| self.canvas.itemconfig( link, fill='green' ) |
| |
| def unhighlight( _event, link=self.link ): |
| "Unhighlight item on mouse exit." |
| self.canvas.itemconfig( link, fill='blue' ) |
| # self.selectItem( None ) |
| |
| self.canvas.tag_bind( self.link, '<Enter>', highlight ) |
| self.canvas.tag_bind( self.link, '<Leave>', unhighlight ) |
| self.canvas.tag_bind( self.link, '<ButtonPress-1>', select ) |
| |
| def finishLink( self, event ): |
| "Finish creating a link" |
| if self.link is None: |
| return |
| source = self.linkWidget |
| c = self.canvas |
| # Since we dragged from the widget, use root coords |
| x, y = self.canvasx( event.x_root ), self.canvasy( event.y_root ) |
| target = self.findItem( x, y ) |
| dest = self.itemToWidget.get( target, None ) |
| if ( source is None or dest is None or source == dest |
| or dest in source.links or source in dest.links ): |
| self.releaseLink( event ) |
| return |
| # For now, don't allow hosts to be directly linked |
| # stags = self.canvas.gettags( self.widgetToItem[ source ] ) |
| # dtags = self.canvas.gettags( target ) |
| # if 'Host' in stags and 'Host' in dtags: |
| # self.releaseLink( event ) |
| # return |
| x, y = c.coords( target ) |
| c.coords( self.link, self.linkx, self.linky, x, y ) |
| self.addLink( source, dest ) |
| # We're done |
| self.link = self.linkWidget = None |
| |
| # Menu handlers |
| |
| def about( self ): |
| "Display about box." |
| about = self.aboutBox |
| if about is None: |
| bg = 'white' |
| about = Toplevel( bg='white' ) |
| about.title( 'About' ) |
| info = self.appName + ': a simple network editor for Mini-CCNx - based on Miniedit ' |
| warning = 'Development version - not entirely functional!' |
| author = 'Carlos Cabral, Jan 2013' |
| author2 = 'Miniedit by Bob Lantz <rlantz@cs>, April 2010' |
| line1 = Label( about, text=info, font='Helvetica 10 bold', bg=bg ) |
| line2 = Label( about, text=warning, font='Helvetica 9', bg=bg ) |
| line3 = Label( about, text=author, font='Helvetica 9', bg=bg ) |
| line4 = Label( about, text=author2, font='Helvetica 9', bg=bg ) |
| line1.pack( padx=20, pady=10 ) |
| line2.pack(pady=10 ) |
| line3.pack(pady=10 ) |
| line4.pack(pady=10 ) |
| hide = ( lambda about=about: about.withdraw() ) |
| self.aboutBox = about |
| # Hide on close rather than destroying window |
| Wm.wm_protocol( about, name='WM_DELETE_WINDOW', func=hide ) |
| # Show (existing) window |
| about.deiconify() |
| |
| def createToolImages( self ): |
| "Create toolbar (and icon) images." |
| |
| # Model interface |
| # |
| # Ultimately we will either want to use a topo or |
| # mininet object here, probably. |
| |
| def addLink( self, source, dest ): |
| "Add link to model." |
| source.links[ dest ] = self.link |
| dest.links[ source ] = self.link |
| self.links[ self.link ] = ( source, dest ) |
| |
| def deleteLink( self, link ): |
| "Delete link from model." |
| pair = self.links.get( link, None ) |
| if pair is not None: |
| source, dest = pair |
| del source.links[ dest ] |
| del dest.links[ source ] |
| if link is not None: |
| del self.links[ link ] |
| |
| def deleteNode( self, item ): |
| "Delete node (and its links) from model." |
| widget = self.itemToWidget[ item ] |
| for link in widget.links.values(): |
| # Delete from view and model |
| self.deleteItem( link ) |
| del self.itemToWidget[ item ] |
| del self.widgetToItem[ widget ] |
| |
| def build( self ): |
| "Build network based on our topology." |
| |
| net = Mininet( topo=None ) |
| |
| # Make controller |
| net.addController( 'c0' ) |
| # Make nodes |
| for widget in self.widgetToItem: |
| name = widget[ 'text' ] |
| tags = self.canvas.gettags( self.widgetToItem[ widget ] ) |
| nodeNum = int( name[ 1: ] ) |
| if 'Switch' in tags: |
| net.addSwitch( name ) |
| elif 'Host' in tags: |
| net.addHost( name, ip=ipStr( nodeNum ) ) |
| else: |
| raise Exception( "Cannot create mystery node: " + name ) |
| # Make links |
| for link in self.links.values(): |
| ( src, dst ) = link |
| srcName, dstName = src[ 'text' ], dst[ 'text' ] |
| src, dst = net.nameToNode[ srcName ], net.nameToNode[ dstName ] |
| src.linkTo( dst ) |
| |
| # Build network (we have to do this separately at the moment ) |
| net.build() |
| |
| return net |
| |
| def start( self ): |
| "Start network." |
| if self.net is None: |
| self.net = self.build() |
| self.net.start() |
| |
| def stop( self ): |
| "Stop network." |
| if self.net is not None: |
| self.net.stop() |
| cleanUpScreens() |
| self.net = None |
| |
| def xterm( self, _ignore=None ): |
| "Make an xterm when a button is pressed." |
| if ( self.selection is None or |
| self.net is None or |
| self.selection not in self.itemToWidget ): |
| return |
| name = self.itemToWidget[ self.selection ][ 'text' ] |
| if name not in self.net.nameToNode: |
| return |
| term = makeTerm( self.net.nameToNode[ name ], 'Host' ) |
| self.net.terms.append( term ) |
| |
| |
| def miniEditImages(): |
| "Create and return images for MiniEdit." |
| |
| # Image data. Git will be unhappy. However, the alternative |
| # is to keep track of separate binary files, which is also |
| # unappealing. |
| |
| return { |
| 'Select': BitmapImage( |
| file='/usr/include/X11/bitmaps/left_ptr' ), |
| |
| 'Switch' : PhotoImage( data=r""" |
| R0lGODlhOgArAMIEAB8aFwB7tQCb343L8P///////////////yH+GlNvZnR3YXJlOiBNaWNyb3NvZnQgT2ZmaWNlACwAAAAAOgArAAAD/ki63P4wykmrvTjr3YYfQigKH7d5Y6qmnjmBayyHg8vAAqDPaUTbowaA13OIahqcyEgEQEbIi7LIGA1FzsaSQK0QfbnH10sMa83VsqX53HLL7sgUTudR5s367F7PEq4CYDJRcngqfgshiAqAMwF3AYdWTCERjSoBjy+ZVItvMg6XIZmaEgOkSmJwlKOkkKSRlaqraaewr7ABhnqBNLmZuL+6vCzCrpvGsB9EH8m5wc7R0sbQ09bT1dOEBLbXwMjeEN7HpuO6Dt3hFObi7Ovj7d7bEOnYD+4V8PfqF/wN/lKsxZPmop6wBwaFzTsRbVvCWzYQmlMW0UKzZCUqatzICLGjx48gKyYAADs= |
| """), |
| 'Host' : PhotoImage( data=r""" |
| R0lGODlhKQAyAMIHAJyeoK+wsrW2uMHCxM7P0Ozt7fn5+f///yH+EUNyZWF0ZWQgd2l0aCBHSU1QACwAAAAAKQAyAAAD63i63P4wykmrvS4cwLv/IEhxRxGeKGBM3pa+X9QeBmxrT3gMNpyLrt6rgcJgisKXgIFMopaLpjMEVUinn2pQ1ImSrN8uGKCVegHn8bl8CqbV7jFbJ47H650592sX4zl6MX9ocIOBLYNvhkxtiYV8eYx0kJSEi2d7WFmSmZqRmIKeHoddoqOcoaZkqIiqq6CtqqQkrq9jnaKzaLW6Wy8DBMHCp7ClPT+ArMY2t1u9Qs3Et6k+W87KtMfW0r6x1d7P2uDYu+LLtt3nQ9ufxeXM7MkOuCnR7UTe6/jyEOqeWj/SYQEowxXBfgYPJAAAOw== |
| """), |
| 'Link': PhotoImage( data=r""" |
| R0lGODlhFgAWAPcAMf//////zP//mf//Zv//M///AP/M///MzP/M |
| mf/MZv/MM//MAP+Z//+ZzP+Zmf+ZZv+ZM/+ZAP9m//9mzP9mmf9m |
| Zv9mM/9mAP8z//8zzP8zmf8zZv8zM/8zAP8A//8AzP8Amf8AZv8A |
| M/8AAMz//8z/zMz/mcz/Zsz/M8z/AMzM/8zMzMzMmczMZszMM8zM |
| AMyZ/8yZzMyZmcyZZsyZM8yZAMxm/8xmzMxmmcxmZsxmM8xmAMwz |
| /8wzzMwzmcwzZswzM8wzAMwA/8wAzMwAmcwAZswAM8wAAJn//5n/ |
| zJn/mZn/Zpn/M5n/AJnM/5nMzJnMmZnMZpnMM5nMAJmZ/5mZzJmZ |
| mZmZZpmZM5mZAJlm/5lmzJlmmZlmZplmM5lmAJkz/5kzzJkzmZkz |
| ZpkzM5kzAJkA/5kAzJkAmZkAZpkAM5kAAGb//2b/zGb/mWb/Zmb/ |
| M2b/AGbM/2bMzGbMmWbMZmbMM2bMAGaZ/2aZzGaZmWaZZmaZM2aZ |
| AGZm/2ZmzGZmmWZmZmZmM2ZmAGYz/2YzzGYzmWYzZmYzM2YzAGYA |
| /2YAzGYAmWYAZmYAM2YAADP//zP/zDP/mTP/ZjP/MzP/ADPM/zPM |
| zDPMmTPMZjPMMzPMADOZ/zOZzDOZmTOZZjOZMzOZADNm/zNmzDNm |
| mTNmZjNmMzNmADMz/zMzzDMzmTMzZjMzMzMzADMA/zMAzDMAmTMA |
| ZjMAMzMAAAD//wD/zAD/mQD/ZgD/MwD/AADM/wDMzADMmQDMZgDM |
| MwDMAACZ/wCZzACZmQCZZgCZMwCZAABm/wBmzABmmQBmZgBmMwBm |
| AAAz/wAzzAAzmQAzZgAzMwAzAAAA/wAAzAAAmQAAZgAAM+4AAN0A |
| ALsAAKoAAIgAAHcAAFUAAEQAACIAABEAAADuAADdAAC7AACqAACI |
| AAB3AABVAABEAAAiAAARAAAA7gAA3QAAuwAAqgAAiAAAdwAAVQAA |
| RAAAIgAAEe7u7t3d3bu7u6qqqoiIiHd3d1VVVURERCIiIhEREQAA |
| ACH5BAEAAAAALAAAAAAWABYAAAhIAAEIHEiwoEGBrhIeXEgwoUKG |
| Cx0+hGhQoiuKBy1irChxY0GNHgeCDAlgZEiTHlFuVImRJUWXEGEy |
| lBmxI8mSNknm1Dnx5sCAADs= |
| """ ) |
| } |
| |
| if __name__ == '__main__': |
| setLogLevel( 'info' ) |
| temp_file = parse_args() |
| app = MiniEdit(template_file=temp_file) |
| app.mainloop() |