Eskil

Artifact [181d2a9318]
Login

Artifact 181d2a931834ac18e3162d96d6fb3db2f639052a723ed4ceec618b046b73fe4b:


#----------------------------------------------------------------------
#  Eskil, Plugin handling
#
#  Copyright (c) 2008-2016, Peter Spjuth  (peter.spjuth@gmail.com)
#
#  This program is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 2 of the License, or
#  (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; see the file COPYING.  If not, write to
#  the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
#  Boston, MA 02111-1307, USA.
#
#----------------------------------------------------------------------
# $Revision$
#----------------------------------------------------------------------

proc PluginSearchPath {} {
    set dirs [list . ./plugins]
    lappend dirs [file join $::eskil(thisDir) .. ..]
    lappend dirs [file join $::eskil(thisDir) .. .. plugins]
    lappend dirs [file join $::eskil(thisDir) .. plugins]
    return $dirs
}

# Locate plugin source and extract some info
# Data structure in this return dict:
# name: Plugin name.
# file: Source file name. "_" for a runtime plugin.
# data: Source code.
# opts: Options accepted by plugin.
proc LocatePlugin {plugin} {
    set res [dict create name "" file "" opts "" data ""]
    set fSrc ""
    set code ""

    # Search runtime plugins first
    foreach name [dict keys $::eskil(plugins)] {
        if {$name eq $plugin} {
            set fSrc "_"
            set code [dict get $::eskil(plugins) $name data]
        }
    }

    if {$fSrc eq ""} {
        foreach dir [PluginSearchPath] {
            set dir [file normalize $dir]
            set files {}
            lappend files [file join $dir $plugin]
            lappend files [file join $dir $plugin.tcl]
            foreach file $files {
                if { ! [file exists   $file]} continue
                if { ! [file isfile   $file]} continue
                if { ! [file readable $file]} continue
                set ch [open $file r]
                set code [read $ch 20]
                close $ch
                # Magic pattern to identify a plugin
                if {[string match "##Eskil Plugin*" $code]} {
                    set fSrc $file
                    break
                }
            }
            if {$fSrc ne ""} break
        }
    }

    if {$fSrc eq "_"} {
        dict set res "name" $plugin
        dict set res "file" $fSrc
        dict set res "data" $code
    } elseif {$fSrc ne ""} {
        dict set res "name" $plugin
        dict set res "file" $fSrc
        # Plugin source is reloaded each time to facilitate debug/rerun.
        set ch [open $fSrc r]
        set code [read $ch]
        close $ch
        dict set res "data" $code
    }

    # Look for declarations of command line options
    foreach line [split $code \n] {
        # Only look until empty line
        if {[string trim $line] eq ""} break
        if {[regexp {^\#\# Option\s+(\S+)(.*)} $line -> name rest]} {
            # structure is name flag doc
            dict lappend res opts $name 0 [string trim $rest " :"]
        }
        if {[regexp {^\#\# Flag\s+(\S+)(.*)} $line -> name rest]} {
            dict lappend res opts $name 1 [string trim $rest " :"]
        }
    }

    return $res
}

# Return value: Handle to interpreter
#
# pinfo dict structure:
#  file : File plugin
#  dir  : Directory plugin
#  allow: Raised privileges
proc createPluginInterp {plugin info allow pinfoName} {
    upvar 1 $pinfoName pinfo
    set res [LocatePlugin $plugin]
    set code [dict get $res data]
    set fSrc [dict get $res file]

    if {$code eq ""} {
        return ""
    }

    # Create interpreter and load source
    if {$allow} {
        set pi [interp create]
        $pi eval $code
    } else {
        set pi [interp create -safe]
        $pi eval $code
    }

    # Setup info
    $pi eval [list set ::WhoAmI [file rootname [file tail $fSrc]]]
    $pi eval [list set ::WhoAmIFull [file normalize $fSrc]]
    $pi eval [list set ::Info $info]
    interp share {} stdout $pi

    # Expose needed commands
    if { ! $allow} {
        interp expose $pi fconfigure ;# needed??
        interp hide $pi close
    }

    set pinfo {file 0 dir 0}
    dict set pinfo "allow" $allow
    if {[$pi eval info proc PreProcess] ne ""} {
        dict set pinfo file 1
    }
    if {[$pi eval info proc FileCompare] ne ""} {
        dict set pinfo dir 1
    }

    return $pi
}

proc printPlugin {plugin {short 0}} {
    set res [LocatePlugin $plugin]
    set fSrc [dict get $res file]
    if {$fSrc eq ""} {
        printPlugins
        return
    }
    foreach line [split [dict get $res data] \n] {
        set lineT [string trim $line]
        if {$short} {
            if { ! [string match "#*" $lineT]} {
                break
            }
        }
        puts $line
    }
}

proc listPlugins {} {
    set dirs [PluginSearchPath]
    set result {}

    foreach name [dict keys $::eskil(plugins)] {
        dict set result $name [dict get $::eskil(plugins) $name]
    }

    foreach dir $dirs {
        set dir [file normalize $dir]
        set files [glob -nocomplain [file join $dir *.tcl]]
        foreach file $files {
            set file [file normalize $file]
            if {[info exists done($file)]} continue
            if { ! [file exists $file]} continue
            if { ! [file isfile $file]} continue
            if { ! [file readable $file]} continue

            set done($file) 1
            set ch [open $file r]
            set code [read $ch 200]
            if {[regexp {^\#\#Eskil Plugin :(.*?)(\n|$)} $code -> descr]} {
                set root [file rootname [file tail $file]]
                dict set result $root "descr" $descr
                dict set result $root "file" 0
                dict set result $root "dir"  0
                # Load it all for inspection
                append code [read $ch]
                dict set result $root "data" $code
            }
        }
    }
    foreach root [dict keys $result] {
        set code [dict get $result $root data]
        if {[regexp {^\#\#Eskil Plugin :(.*?)(\n|$)} $code -> descr]} {
            dict set result $root "descr" $descr
        }
        if {[string first "proc PreProcess " $code] >= 0} {
            dict set result $root "file" 1
        }
        if {[string first "proc FileCompare " $code] >= 0} {
            dict set result $root "dir" 1
        }
    }

    set resultSort {}
    foreach elem [lsort -dictionary [dict keys $result]] {
        dict set resultSort $elem [dict get $result $elem]
    }
    return $resultSort
}

proc printPlugins {} {
    set plugins [listPlugins]
    if {[llength $plugins] == 0} {
        puts "No plugins found."
        return
    }
    # Longest name?
    set w 0
    foreach {plugin info} $plugins {
        if {[string length $plugin] > $w} {
            set w [string length $plugin]
        }
    }
    # Room for quote marks in output
    incr w 2

    puts "Available plugins:"
    foreach {plugin info} $plugins {
        set descr [dict get $info descr]
        puts "Plugin [format %-*s $w \"$plugin\"] : $descr"
    }
}

# Handle plugins for a diff session that uses plugins.
# Returns true if something has been done that needs cleanup.
proc preparePlugin {top} {
    if {$::eskil($top,plugin,1) eq "" || \
                ![dict get $::eskil($top,pluginpinfo,1) file]} {
        return 0
    }

    disallowEdit $top
    set in1 $::eskil($top,leftFile)
    set in2 $::eskil($top,rightFile)

    foreach item [lsort -dictionary [array names ::eskil $top,pluginname,*]] {
        set pI [lindex [split $item ","] end]

        set allow [dict get $::eskil($top,pluginpinfo,$pI) allow]
        # Pass ::argv to plugin
        set pArgv $::eskil(argv)
        if {[info exists ::eskil($top,pluginargv,$pI)]} {
            lappend pArgv {*}$::eskil($top,pluginargv,$pI)
        }
        $::eskil($top,plugin,$pI) eval [list set ::argv $pArgv]
        # Pass ::Pref to plugin
        $::eskil($top,plugin,$pI) eval [list array set ::Pref [array get ::Pref]]
        # Pass File info to plugin
        $::eskil($top,plugin,$pI) eval [list set ::File(left)  $::eskil($top,leftFile)]
        $::eskil($top,plugin,$pI) eval [list set ::File(right) $::eskil($top,rightFile)]

        set out1 [tmpFile]
        set out2 [tmpFile]

        set chi [open $in1 r]
        set cho [open $out1 w]
        set chi2 [open $in2 r]
        set cho2 [open $out2 w]
        interp share {} $chi $::eskil($top,plugin,$pI)
        interp share {} $cho $::eskil($top,plugin,$pI)
        interp share {} $chi2 $::eskil($top,plugin,$pI)
        interp share {} $cho2 $::eskil($top,plugin,$pI)

        set cmd1 [list PreProcess left $chi $cho]
        set cmd2 [list PreProcess right $chi2 $cho2]
        if {[info commands yield] ne ""} {
            # When in 8.6, this is done in coroutines allowing each call
            # to yield and to alternate between them until done
            set c1 __plugin_cr1$top
            set c2 __plugin_cr2$top
            set cmd1 [linsert $cmd1 0 coroutine $c1]
            set cmd2 [linsert $cmd2 0 coroutine $c2]
            set usenew1 [$::eskil($top,plugin,$pI) eval $cmd1]
            set usenew2 [$::eskil($top,plugin,$pI) eval $cmd2]
            interp alias {} pnw $::eskil($top,plugin,$pI) namespace which
            while {[pnw $c1] ne {} || [pnw $c2] ne {}} {
                if {[pnw $c1] ne {}} {
                    set usenew1 [$::eskil($top,plugin,$pI) eval $c1]
                }
                if {[pnw $c2] ne {}} {
                    set usenew2 [$::eskil($top,plugin,$pI) eval $c2]
                }
            }
        } else {
            set usenew1 [$::eskil($top,plugin,$pI) eval $cmd1]
            set usenew2 [$::eskil($top,plugin,$pI) eval $cmd2]
        }

        if {$allow} {
            $::eskil($top,plugin,$pI) eval close $chi
            $::eskil($top,plugin,$pI) eval close $cho
            $::eskil($top,plugin,$pI) eval close $chi2
            $::eskil($top,plugin,$pI) eval close $cho2
        } else {
            $::eskil($top,plugin,$pI) invokehidden close $chi
            $::eskil($top,plugin,$pI) invokehidden close $cho
            $::eskil($top,plugin,$pI) invokehidden close $chi2
            $::eskil($top,plugin,$pI) invokehidden close $cho2
        }
        close $chi
        close $cho
        close $chi2
        close $cho2

        if {$usenew1} {
            # The file after processing should be used both
            # for comparison and for displaying.
            if { ! [info exists ::eskil($top,leftFileBak)]} {
                set ::eskil($top,leftFileBak) $::eskil($top,leftFile)
            }
            unset -nocomplain ::eskil($top,leftFileDiff)
            set ::eskil($top,leftFile) $out1
        } else {
            set ::eskil($top,leftFileDiff) $out1
        }
        if {$usenew2} {
            if { ! [info exists ::eskil($top,rightFileBak)]} {
                set ::eskil($top,rightFileBak) $::eskil($top,rightFile)
            }
            unset -nocomplain ::eskil($top,rightFileDiff)
            set ::eskil($top,rightFile) $out2
        } else {
            set ::eskil($top,rightFileDiff) $out2
        }
        # For next plugin, if any
        set in1 $out1
        set in2 $out2
    }
    return 1
}

# After diff is done, this is called if preparePlugin returned true.
proc cleanupPlugin {top} {
    if {[info exists ::eskil($top,leftFileBak)]} {
        set ::eskil($top,leftFile) $::eskil($top,leftFileBak)
    }
    if {[info exists ::eskil($top,rightFileBak)]} {
        set ::eskil($top,rightFile) $::eskil($top,rightFileBak)
    }
    unset -nocomplain \
            ::eskil($top,leftFileBak) ::eskil($top,rightFileBak) \
            ::eskil($top,leftFileDiff) ::eskil($top,rightFileDiff)
}

# GUI for plugin selection
proc editPrefPlugins {top {dirdiff 0}} {
    set wt $top.prefplugin

    # Create window
    destroy $wt
    toplevel $wt -padx 3 -pady 3
    ttk::frame $wt._bg
    place $wt._bg -x 0 -y 0 -relwidth 1.0 -relheight 1.0 -border outside
    wm title $wt "Preferences: Plugins"

    ttk::notebook $wt.tab
    ttk::frame $wt.tab.plus
    $wt.tab add $wt.tab.plus -text "+"
    set n [llength [array names ::eskil $top,pluginname,*]]
    if {$n < 1} { set n 1 }
    for {set t 0} {$t < $n} {incr t} {
        EditPrefPluginsAddTab $top $dirdiff
    }
    $wt.tab select 0
    bind $wt.tab <<NotebookTabChanged>> \
            [list EditPrefPluginsChangeTab $top $dirdiff]
    bind $wt.tab <ButtonPress-3> \
            [list EditPrefPluginsRightClick $top $dirdiff %x %y %X %Y]

    ttk::frame $wt.fb -padding 3
    ttk::button $wt.fb.b1 -text "Ok"   \
            -command [list EditPrefPluginsOk $top $wt 0]
    ttk::button $wt.fb.b2 -text "Apply" \
            -command [list EditPrefPluginsOk $top $wt 1]
    ttk::button $wt.fb.b3 -text "Cancel" -command [list destroy $wt]
    set ::widgets($top,prefPluginsOk) $wt.fb.b1

    grid $wt.fb.b1 x $wt.fb.b2 x $wt.fb.b3 -sticky we
    grid columnconfigure $wt.fb {0 2 4} -uniform a
    grid columnconfigure $wt.fb {1 3} -weight 1

    grid $wt.tab -sticky news -padx 3 -pady 3
    grid $wt.fb  -sticky we   -padx 3 -pady 3
    grid columnconfigure $wt 0 -weight 1
    grid row             $wt 0 -weight 1

}

# Detect a plugin tab change to add tab when "+" is selected.
proc EditPrefPluginsChangeTab {top dirdiff} {
    set wt $top.prefplugin.tab
    set n [$wt index end]
    set t [$wt index [$wt select]]

    if {$t + 1 == $n} {
        # Plus selected
        EditPrefPluginsAddTab $top $dirdiff
        $wt select $t
    }
}

# Context menu
proc EditPrefPluginsRightClick {top dirdiff x y X Y} {
    set wt $top.prefplugin.tab
    set elem [$wt identify element $x $y]
    set t [$wt identify tab $x $y]
    if {$elem eq "" || ![string is integer -strict $t]} return

    set m [winfo toplevel $wt].pm
    destroy $m
    menu $m
    set n [$wt index end]

    $m add command -label "Add left" \
            -command [list EditPrefPluginsAddTab $top $dirdiff $t]
    if {$t > 0 && $t < ($n - 1)} {
        $m add command -label "Move left" \
                -command [list EditPrefPluginsMoveLeft $top $t]
    }
    tk_popup $m $X $Y
}

# Move a tab to the left
proc EditPrefPluginsMoveLeft {top pos} {
    set wt $top.prefplugin.tab
    set win [lindex [$wt tabs] $pos]
    incr pos -1
    $wt insert $pos $win
}

# Add a tab to plugin prefernces
proc EditPrefPluginsAddTab {top dirdiff {pos {}}} {
    set wt $top.prefplugin.tab
    set pI [$wt index end]
    if {$pos eq "" || $pos >= ($pI - 1)} {
        # Since the "+" tab is last, the index is n for any new one
        set pos [expr {$pI - 1}]
    }
    ttk::frame $wt.f,$pI
    $wt insert $pos $wt.f,$pI -text "Plugin"

    set wt $wt.f,$pI

    set plugins [listPlugins]
    if {[llength $plugins] == 0} {
        grid [ttk::label $wt.l -text "No plugins found."] - -padx 3 -pady 3
    }
    if { ! [info exists ::eskil($top,pluginname,$pI)]} {
        set ::eskil($top,pluginname,$pI) ""
    }
    if { ! [info exists ::eskil($top,plugininfo,$pI)]} {
        set ::eskil($top,plugininfo,$pI) ""
    }
    if { ! [info exists ::eskil($top,pluginallow,$pI)]} {
        set ::eskil($top,pluginallow,$pI) 0
    }
    set ::eskil($top,edit,pluginname,$pI) $::eskil($top,pluginname,$pI)
    set ::eskil($top,edit,plugininfo,$pI) $::eskil($top,plugininfo,$pI)
    set ::eskil($top,edit,pluginallow,$pI) $::eskil($top,pluginallow,$pI)

    ttk::labelframe $wt.lfs -text "Select"
    grid columnconfigure $wt.lfs 1 -weight 1

    set t 0
    foreach {plugin info} $plugins {
        set descr [dict get $info descr]
        if {$dirdiff && ![dict get $info dir]} continue
        ttk::radiobutton $wt.rb$t -variable ::eskil($top,edit,pluginname,$pI) \
                -value $plugin -text $plugin -command "SelectPlugin $top $pI $plugin"
        ttk::label $wt.l$t -text $descr -anchor w
        grid $wt.rb$t $wt.l$t -  - -in $wt.lfs -sticky we -padx 3 -pady 3
        incr t
    }
    ttk::radiobutton $wt.rb$t -variable ::eskil($top,edit,pluginname,$pI) \
            -value "" -text "No Plugin" -command "SelectPlugin $top $pI $plugin"
    ttk::button $wt.bs -text "Show" -state disable \
            -command "ShowPlugin $wt \$::eskil($top,edit,pluginname,$pI)"
    addBalloon $wt.bs "Show plugin source code."
    ttk::button $wt.bc -text "Clone" -state disable \
            -command "ClonePlugin $wt \$::eskil($top,edit,pluginname,$pI)"
    addBalloon $wt.bc "Clone to a runtime plugin."
    ttk::button $wt.be -text "Edit" -state disable \
            -command "EditPlugin $wt \$::eskil($top,edit,pluginname,$pI)"
    set ::eskil($top,edit,showW,$pI) $wt.bs
    set ::eskil($top,edit,cloneW,$pI) $wt.bc
    set ::eskil($top,edit,editW,$pI) $wt.be
    addBalloon $wt.be "Edit a runtime plugin."
    SelectPlugin $top $pI $::eskil($top,edit,pluginname,$pI)

    grid $wt.rb$t $wt.be $wt.bc $wt.bs -in $wt.lfs -sticky we -padx 3 -pady 3
    grid $wt.bs $wt.bc $wt.be -sticky e

    ttk::labelframe $wt.lfgc -text "Generic Configuration"
    grid columnconfigure $wt.lfgc 1 -weight 1

    ttk::label $wt.li -text "Info" -anchor w
    addBalloon $wt.li "Info passed to plugin. Plugin specific."
    ttk::entry $wt.ei -textvariable ::eskil($top,edit,plugininfo,$pI)
    grid $wt.li $wt.ei -in $wt.lfgc -sticky we -padx 3 -pady 3

    ttk::checkbutton $wt.cb -text "Privilege" \
            -variable ::eskil($top,edit,pluginallow,$pI)
    addBalloon $wt.cb "Run plugin with raised privileges"
    grid $wt.cb -  -in $wt.lfgc -sticky w -padx 3 -pady 3

    ttk::labelframe $wt.lfsc -text "Specific Configuration"
    set ::widgets($top,prefPluginsSpec,$pI) $wt.lfsc
    trace add variable ::eskil($top,edit,pluginname,$pI) write \
            [list UpdateSpecificPluginConf $top $pI]
    UpdateSpecificPluginConf $top $pI

    grid $wt.lfs  -sticky we -padx 3 -pady 3
    grid $wt.lfgc -sticky we -padx 3 -pady 3
    grid $wt.lfsc -sticky we -padx 3 -pady 3
    grid columnconfigure $wt 0 -weight 1
}

# When a new plugin is selected, update the list of specific options.
# "args" is needed to swallow the extra variable trace args.
proc UpdateSpecificPluginConf {top pI args} {
    set w $::widgets($top,prefPluginsSpec,$pI)
    # If the dialog is closed w might not exist
    if { ! [winfo exists $w]} return
    eval destroy [winfo children $w]

    set arg $::eskil($top,edit,pluginname,$pI)
    set pOpts {}
    if {$arg ne ""} {
        set res [LocatePlugin $arg]
        set pOpts [dict get $res opts]
    }
    # Look for defaults on the command line
    set pArgv $::eskil(argv)
    if {[info exists ::eskil($top,pluginargv,$pI)]} {
        lappend pArgv {*}$::eskil($top,pluginargv,$pI)
    }
    # Look for declarations of command line options
    set t 0
    set ::eskil($top,edit,opts,$pI) $pOpts
    foreach {name flag doc} $pOpts {
        ttk::label $w.l$t -text $name
        addBalloon $w.l$t -fmt $doc
        grid $w.l$t -sticky "w" -padx 3 -pady 3
        if {$flag} {
            # Initialise if given.
            if {[lsearch -exact $pArgv $name] >= 0} {
                set ::eskil($top,edit,$name,$pI) 1
                # Move responsibility from global argv
                set ix [lsearch -exact $::eskil(argv) $name]
                if {$ix >= 0} {
                    set ::eskil(argv) [lreplace $::eskil(argv) $ix $ix]
                    lappend ::eskil($top,pluginargv,$pI) $name
                }
            }
            ttk::checkbutton $w.s$t -text "On" \
                    -variable ::eskil($top,edit,$name,$pI)
            grid $w.s$t -row $t -column 1 -sticky "w" -padx 3 -pady 3
        } else {
            # Initialise if given.
            set ix [lsearch -exact $pArgv $name]
            if {$ix >= 0} {
                set ::eskil($top,edit,$name,$pI) [lindex $pArgv $ix+1]
                # Move responsibility from global argv
                set ix [lsearch -exact $::eskil(argv) $name]
                if {$ix >= 0} {
                    lappend ::eskil($top,pluginargv,$pI) $name \
                            [lindex $::eskil(argv) $ix+1]
                    set ::eskil(argv) [lreplace $::eskil(argv) $ix $ix+1]
                }
            }
            ttk::entry $w.s$t \
                    -textvariable ::eskil($top,edit,$name,$pI)
            grid $w.s$t -row $t -column 1 -sticky we -padx 3 -pady 3
        }
        incr t
    }
    grid columnconfigure $w 1 -weight 1
    if {$t == 0} {
        ttk::label $w.l -text "No specific configuration"
        grid $w.l -sticky "w" -padx 3 -pady 3
        return
    }
}

# Ok or Apply pressend in Plugin Preference
proc EditPrefPluginsOk {top wt apply} {
    # Compress plugin info in tab order
    set allN {}
    foreach win [$wt.tab tabs] {
        set pI [lindex [split $win ","] end]
        if { ! [string is integer -strict $pI]} continue
        # Find all used.
        if {$::eskil($top,edit,pluginname,$pI) ne ""} {
            lappend allN $pI
        }
    }
    if {[llength $allN] == 0} {
        lappend allN 1
    }

    # Keep the dialog if we are only applying
    if { ! $apply} {
        destroy $wt
    }

    # Transfer them to consecutive numbers
    set t 1
    foreach pI $allN {
        set ::eskil($top,pluginname,$t)  $::eskil($top,edit,pluginname,$pI)
        set ::eskil($top,plugininfo,$t)  $::eskil($top,edit,plugininfo,$pI)
        set ::eskil($top,pluginallow,$t) $::eskil($top,edit,pluginallow,$pI)
        incr t
    }
    # Remove any old
    foreach item [array names ::eskil $top,pluginname,*] {
        set pI [lindex [split $item ","] end]
        if {$pI >= $t} {
            unset ::eskil($top,pluginname,$pI)
            set ::eskil($top,plugininfo,$pI) ""
            set ::eskil($top,pluginallow,$pI) 0
        }
    }

    # Handle all plugins
    foreach item [array names ::eskil $top,pluginname,*] {
        set pI [lindex [split $item ","] end]
        if {$::eskil($top,pluginname,$pI) ne ""} {
            set pinterp [createPluginInterp $::eskil($top,pluginname,$pI) \
                                 $::eskil($top,plugininfo,$pI) \
                                 $::eskil($top,pluginallow,$pI) pinfo]
        } else {
            set pinterp ""
            set pinfo ""
        }
        set ::eskil($top,plugin,$pI) $pinterp
        set ::eskil($top,pluginpinfo,$pI) $pinfo
        set ::eskil($top,pluginargv,$pI) {}
        foreach {name flag doc} $::eskil($top,edit,opts,$pI) {
            if {$flag} {
                if {[info exists ::eskil($top,edit,$name,$pI)] && \
                            $::eskil($top,edit,$name,$pI)} {
                    lappend ::eskil($top,pluginargv,$pI) $name
                }
            } else {
                if {[info exists ::eskil($top,edit,$name,$pI)] && \
                            $::eskil($top,edit,$name,$pI) ne ""} {
                    lappend ::eskil($top,pluginargv,$pI) $name \
                            $::eskil($top,edit,$name,$pI)
                }
            }
        }
    }
}

# Put Tcl code in a text widget, with some syntax highlighting
proc TextViewTcl {tW data} {
    $tW tag configure comment -foreground "#b22222"
    foreach line [split $data \n] {
        if {[regexp {^\s*#} $line]} {
            $tW insert end $line\n comment
        } elseif {[regexp {^(.*;\s*)(#.*)$} $line -> pre post]} {
            $tW insert end $pre
            $tW insert end $post\n comment
        } else {
            $tW insert end $line\n
        }
    }
}

proc SelectPlugin {top pI plugin} {
    $::eskil($top,edit,showW,$pI)  configure -state disable
    $::eskil($top,edit,cloneW,$pI) configure -state disable
    $::eskil($top,edit,editW,$pI)  configure -state disable
    if {$plugin eq ""} {
        return
    }

    $::eskil($top,edit,showW,$pI)  configure -state normal
    # TODO: Enable when this works.
    #$::eskil($top,edit,cloneW,$pI) configure -state normal
    foreach name [dict keys $::eskil(plugins)] {
        if {$name eq $plugin} {
            # TODO: Enable when this works.
            #$::eskil($top,edit,editW,$pI)  configure -state normal
        }
    }
}

proc EditPlugin {parent plugin} {
    # TODO
}

proc ClonePlugin {parent plugin} {
    set res [LocatePlugin $plugin]
    dict set res name clone_$plugin
    dict set ::eskil(plugins) clone_$plugin $res
}

# Show plugin source
proc ShowPlugin {parent plugin} {
    set res [LocatePlugin $plugin]
    set data [dict get $res data]
    if {$data eq ""} return

    set wt $parent.plugin
    if {[winfo exists $wt]} {
        wm deiconify $wt
    } else {
        toplevel $wt -padx 3 -pady 3
    }
    destroy {*}[winfo children $wt]
    ttk::frame $wt._bg
    place $wt._bg -x 0 -y 0 -relwidth 1.0 -relheight 1.0 -border outside
    wm title $wt "Plugin: $plugin"

    set t [Scroll both text $wt.t -width 80 -height 30 -font myfont -wrap none]
    pack $wt.t -fill both -expand 1
    bind $t <Control-a> "[list $t tag add sel 1.0 end];break"

    TextViewTcl $t $data
}