#----------------------------------------------------------------------
# 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
}