#!/usr/bin/env wish
# Copyright 2018-2025 Siep Kroonenberg
# This file is licensed under the GNU General Public License version 2
# or any later version.
# common declarations for tlshell.tcl and install-tl-gui.tcl
set ::plain_unix 0
if {$::tcl_platform(platform) eq "unix" &&
[string range $::tcl_version 0 1] eq "8." && $::tcl_platform(os) ne "Darwin"} {
set ::plain_unix 1
}
if $::plain_unix {
# plain_unix: avoid a RenderBadPicture error on quitting.
# 'send' changes the shutdown sequence,
# which avoids triggering the bug.
# 'tk appname <something>' restores 'send' and avoids the bug
bind . <Destroy> {
catch {tk appname appname}
}
}
# process ID of the perl program that will run in the background
set ::perlpid 0
# mirrors
set any_mirror "
https://mirror.ctan.org/systems/texlive/tlnet"
# turn name into a string suitable for a widget name
proc mangle_name {n} {
set n [string tolower $n]
set n [string map {" " "_"} $n]
return $n
} ; # mangle_name
set mirrors [dict create]
proc read_mirrors {} {
if [catch {open [file join $::instroot \
"tlpkg/installer/ctan-mirrors.pl"] r} fm] {
return 0
}
set re_geo {^\s*'([^']+)' => \{\s*$}
set re_url {^\s*'(.*)' => ([0-9]+)}
set re_clo {^\s*\},?\s*$}
set starting 1
set lnum 0 ; # line number for error messages
set ok 1 ; # no errors encountered yet
set countries {} ; # aggregate list of countries
set urls {} ; # aggregate list of urls
set continent ""
set country ""
set u ""
set in_cont 0
set in_coun 0
while {! [catch {chan gets $fm} line] && ! [chan eof $fm]} {
incr lnum
if $starting {
if {[string first "\$mirrors =" $line] == 0} {
set starting 0
continue
} else {
set ok 0
set msg "Unexpected line '$line' at start"
break
}
}
# starting is now dealt with.
if [regexp $re_geo $line dummy c] {
if {! $in_cont} {
set in_cont 1
set continent $c
set cont_dict [dict create]
if {$continent in [dict keys $::mirrors]} {
set ok 0
set msg "Duplicate continent $c at line $lnum"
break
}
} elseif {! $in_coun} {
set in_coun 1
set country $c
if {$country in $countries} {
set ok 0
set msg "Duplicate country $c at line $lnum"
break
}
lappend countries $country
dict set cont_dict $country {}
} else {
set ok 0
set msg "Unexpected continent- or country line $line at line $lnum"
break
}
} elseif [regexp $re_url $line dummy u n] {
if {! $in_coun} {
set ok 0
set msg "Unexpected url line $line at line $lnum"
break
} elseif {$n ne "1"} {
continue
}
append u "systems/texlive/tlnet"
if {$u in $urls} {
set ok 0
set msg "Duplicate url $u at line $lnum"
break
}
dict lappend cont_dict $country $u
lappend urls $u
set u ""
} elseif [regexp $re_clo $line] {
if $in_coun {
set in_coun 0
set country ""
} elseif $in_cont {
set in_cont 0
dict set ::mirrors $continent $cont_dict
set continent ""
} else {
break ; # should close mirror list
}
} ; # ignore other lines
}
close $fm
} ; # read_mirrors
# cascading dropdown mirror menu
# parameter cmd should be a proc which does something with the selected url
proc mirror_menu {wnd cmd} {
destroy $wnd.m
if {[dict size $::mirrors] == 0} read_mirrors
if {[dict size $::mirrors] > 0} {
ttk::menubutton $wnd -text [__ "Specific mirror..."] \
-direction below -menu $wnd.m
menu $wnd.m
dict for {cont d_cont} $::mirrors {
set c_ed [mangle_name $cont]
menu $wnd.m.$c_ed
$wnd.m add cascade -label $cont -menu $wnd.m.$c_ed
dict for {cntr urls} $d_cont {
set n_ed [mangle_name $cntr]
menu $wnd.m.$c_ed.$n_ed
$wnd.m.$c_ed add cascade -label $cntr -menu $wnd.m.$c_ed.$n_ed
foreach u $urls {
$wnd.m.$c_ed.$n_ed add command -label $u -command "$cmd $u"
}
}
}
} else {
ttk::label $wnd -text [__ "No mirror list available"]
}
return $wnd
}
proc possible_repository {s} {
if [regexp {^(https?|ftp|scp|ssh):\/\/.+} $s] {return 1}
if {[string first {file://} $s] == 0} {set s [string range $s 7 end]}
if [file isdirectory [file join $s "archive"]] {return 1}
if [file isdirectory [file join $s "texmf-dist/web2c"]] {return 1}
return 0
}
proc get_stacktrace {} {
set level [info level]
set s ""
for {set i 1} {$i < $level} {incr i} {
append s [format "Level %u: %s\n" $i [info level $i]]
}
return $s
} ; # get_stacktrace
proc normalize_argv {} {
# work back to front, to not disturb indices of unscanned list elements
set i $::argc
while 1 {
incr i -1
if {$i<0} break
set s [lindex $::argv $i]
if {[string range $s 0 1] eq "--"} {
set s [string range $s 1 end]
lset ::argv $i $s
}
set j [string first "=" $s]
if {$j > 0} {
set s0 [string range $s 0 [expr {$j-1}]]
set s1 [string range $s [expr {$j+1}] end]
set ::argv [lreplace $::argv $i $i $s0 $s1]
} elseif {$j==0} {
err_exit "Command-line argument $s starting with \"=\""
} ; # else leave alone
}
set ::argc [llength $::argv]
}
normalize_argv
# set width of a treeview column wide enough
# to fully display all entries
proc set_tree_col_width {tv cl} {
set len 0
foreach c [$tv children {}] {
# '<pathname> set <item> <column>' without a value parameter
# is really a get.
# Tree cells are set to use TkDefaultFont redo_fonts further down.
set l [font measure TkDefaultFont [$tv set $c $cl]]
if {$l > $len} {set len $l}
}
$tv column $cl -width [expr {$len+10}]
}
# localization support
# for the sake of our translators we use our own translation function
# which can use .po files directly. This allows them to check their work
# without creating or waiting for a conversion to .msg.
# We still use the msgcat module for detecting default locale.
# Otherwise, the localization code borrows much from Norbert Preining's
# translation module for TL.
package require msgcat
# available languages
set ::langs [list "en"]
foreach l [glob -nocomplain -directory \
[file join $::instroot "tlpkg" "translations"] *.po] {
lappend ::langs [string range [file tail $l] 0 end-3]
}
proc initialize_language {} {
# check the command-line for a lang parameter
set ::lang ""
set i 0
while {$i < $::argc} {
set p [lindex $::argv $i]
incr i
if {$p eq "-lang" || $p eq "-gui-lang"} {
if {$i < $::argc} {
set ::lang [lindex $::argv $i]
break
}
}
}
unset i
# First fallback, only for tlshell: check tlmgr config file
if {$::lang eq "" && [info exists ::invoker] && $::invoker eq "tlshell"} {
set ::lang [get_config_var "gui-lang"]
}
# try to set tcltk's locale to $::lang too. this may not work for 8.5.
if {$::lang ne ""} {::msgcat::mclocale $::lang}
# second fallback: what does msgcat think about it? Note that
# msgcat checks the environment and on windows also the registry.
if {$::lang eq ""} {set ::lang [::msgcat::mclocale]}
set messcat ""
if {$::lang ne ""} {
set messcat ""
set maybe ""
set ::lang [string tolower $::lang]
set tdir [file join $::instroot "tlpkg" "translations"]
foreach f [glob -nocomplain -directory $tdir *.po] {
set ln_f [string tolower [string range [file tail $f] 0 end-3]]
if {$ln_f eq $::lang} {
set messcat $f
break
} elseif {[string range $ln_f 0 1] eq [string range $::lang 0 1]} {
set maybe $f
}
}
if {$messcat eq "" && $maybe ne ""} {
set ::lang [string tolower [string range [file tail $maybe] 0 end-3]]
}
}
}
initialize_language
proc load_translations {} {
array unset ::TRANS
if {$::lang eq ""} return
set messcat [file join $::instroot "tlpkg" "translations" "${::lang}.po"]
# parse messcat.
# skip lines which make no sense
if [file exists $messcat] {
# create array with msgid keys and msgstr values
# in the case that we switch languages,
# we need to remove old translations,
# since the new set may not completely cover the old one
if {! [catch {open $messcat r} fid]} {
fconfigure $fid -encoding utf-8
set inmsgid 0
set inmsgstr 0
set msgid ""
set msgstr ""
while 1 {
if [chan eof $fid] break
if [catch {chan gets $fid} l] break
if [regexp {^\s*#} $l] continue
if [regexp {^\s*$} $l] {
# empty line separates msgid/msgstr pairs
if $inmsgid {
# msgstr lines missing
# puts stderr "no translation for $msgid in $messcat"
set msgid ""
set msgstr ""
set inmsgid 0
set inmsgstr 0
continue
}
if $inmsgstr {
# empty line signals end of msgstr
if {$msgstr ne ""} {
# unescape some characters
set msgid [string map {{\n} "\n"} $msgid]
set msgstr [string map {{\n} "\n"} $msgstr]
set msgid [string map {{\\} "\\"} $msgid]
set msgstr [string map {{\\} "\\"} $msgstr]
set msgid [string map {{\"} "\""} $msgid]
set msgstr [string map {{\"} "\""} $msgstr]
set ::TRANS($msgid) $msgstr
}
set msgid ""
set msgstr ""
set inmsgid 0
set inmsgstr 0
continue
}
continue
} ; # empty line
if [regexp {^msgid\s+"(.*)"\s*$} $l m msgid] {
# note. a failed match will leave msgid alone
set inmsgid 1
continue
}
if [regexp {^"(.*)"\s*$} $l m s] {
if $inmsgid {
append msgid $s
} elseif $inmsgstr {
append msgstr $s
}
continue
}
if [regexp {^msgstr\s+"(.*)"\s*$} $l m msgstr] {
set inmsgstr 1
set inmsgid 0
}
}
chan close $fid
}
}
}
load_translations
proc __ {s args} {
if {[info exists ::TRANS($s)]} {
set s $::TRANS($s)
#} else {
# puts stderr "No translation found for $s\n[get_stacktrace]"
}
if {$args eq ""} {
return $s
} else {
return [format $s {*}$args]
}
}
# string representation of booleans
proc yes_no {b} {
if $b {
set ans [__ "Yes"]
} else {
set ans [__ "No"]
}
return $ans
}
# avoid warnings from tar and perl about locale
set ::env(LC_ALL) "C"
unset -nocomplain ::env(LANG)
unset -nocomplain ::env(LANGUAGE)
### fonts ###
# ttk defaults use TkDefaultFont and TkHeadingFont
# ttk classic theme also uses TkTextFont for TEntry
# ttk::combobox uses TkTextFont
# although only the first three appear to be used here, this may depend
# on the theme, so I resize all symbolic fonts anyway.
set dflfonts [list \
TkHeadingFont \
TkCaptionFont \
TkDefaultFont \
TkMenuFont \
TkTextFont \
TkTooltipFont \
TkFixedFont \
TkIconFont \
TkSmallCaptionFont \
]
foreach f $::dflfonts {
set ::oldsize($f) [font configure $f -size]
}
font create bfont
font create lfont
font create hfont
font create titlefont
proc redo_fonts {} {
# note that ttk styles refer to the above symbolic font names
# and do not define fonts themselves
foreach f $::dflfonts {
font configure $f -size [expr { round($::oldsize($f)*$::tkfontscale)}]
}
# the above works for ttk::*button, ttk::treeview, notebook labels
unset -nocomplain f
option add *font TkDefaultFont
# the above works for menu items, ttk::label, text, ttk::entry
# including current value of ttk::combobox, ttk::combobox list items
# and non-ttk labels and buttons - which are not used here
# apparently, these widget classes use the X11 default font on Linux.
set ::cw \
[expr {max([font measure TkDefaultFont "0"],[font measure TkTextFont "0"])}]
# height: assume height == width*2
# workaround for treeview on windows on HiDPI displays
ttk::style configure Treeview -rowheight [expr {3 * $::cw}]
ttk::style configure Cell -font TkDefaultFont
# no bold text for messages; `userDefault' indicates priority
option add *Dialog.msg.font TkDefaultFont userDefault
# normal size bold
font configure bfont {*}[font configure TkDefaultFont]
font configure bfont -weight bold
# larger, not bold: lfont
font configure lfont {*}[font configure TkDefaultFont]
font configure lfont -size [expr {round(1.2 * [font actual lfont -size])}]
# larger and bold
font configure hfont {*}[font configure lfont]
font configure hfont -weight bold
# extra large and bold
font configure titlefont {*}[font configure TkDefaultFont]
font configure titlefont -weight bold \
-size [expr {round(1.5 * [font actual titlefont -size])}]
if $::plain_unix {
ttk::setTheme default ; # or classic.
# the settings below do not work right with clam and alt themes.
ttk::style configure TCombobox -arrowsize [expr {1.5*$::cw}]
ttk::style configure Item -indicatorsize [expr {1.5*$::cw}]
}
}
# initialize scaling factor
set ::tkfontscale ""
if {[info exists ::invoker] && $::invoker eq "tlshell"} {
set ::tkfontscale [get_config_var "tkfontscale"]
# is $::tkfontscale a number, and a reasonable one?
if {[scan $::tkfontscale {%f} f] != 1} { ; # not a number
set ::tkfontscale ""
} elseif {$::tkfontscale < 0} {
set ::tkfontscale ""
} elseif {$::tkfontscale < 0.5} {
set ::tkfontscale 0.5
} elseif {$::tkfontscale > 10} {
set ::tkfontscale 10
}
}
# most systems with a HiDPI display will be configured for it.
# set therefore the default simply to 1.
# users still have the option to scale fonts via the menu.
if {$::tkfontscale eq ""} {set ::tkfontscale 1}
redo_fonts
# icon
catch {
image create photo tl_logo -file \
[file join $::instroot "tlpkg" "tltcl" "tlmgr.gif"]
wm iconphoto . -default tl_logo
}
# default foreground color and disabled foreground color
# may not be black in e.g. dark color schemes
set blk [ttk::style lookup TButton -foreground]
set gry [ttk::style lookup TButton -foreground disabled]
# 'default' padding
proc ppack {wdg args} { ; # pack command with padding
pack $wdg {*}$args -padx 3p -pady 3p
}
proc pgrid {wdg args} { ; # grid command with padding
grid $wdg {*}$args -padx 3p -pady 3p
}
# unicode symbols as fake checkboxes in ttk::treeview widgets
proc mark_sym {mrk} {
if {$::tcl_platform(platform) eq "windows"} {
# under windows, these look slightly better than
# the non-windows selections
if $mrk {
return "\u2714" ; # 'heavy check mark'
} else {
return "\u25CB" ; # 'white circle'
}
} else {
if $mrk {
return "\u25A3" ; # 'white square containing black small square'
} else {
return "\u25A1" ; # 'white square'
}
}
} ; # mark_sym
# for help output
set ::env(NOPERLDOC) 1
##### dialog support #####
# for example code, look at dialog.tcl, part of Tk itself
# In most cases, it is not necessary to explicitly define a handler for
# the WM_DELETE_WINDOW protocol. But if the cancel- or abort button would do
# anything special, then the close icon should not bypass this.
# widget classes which can be enabled and disabled.
# The text widget class is not included here.
set ::active_cls [list TButton TCheckbutton TRadiobutton TEntry Treeview]
# global variable for dialog return value, in case the outcome
# must be handled by the caller rather than by the dialog itself:
set ::dialog_ans {}
# start new toplevel with settings appropriate for a dialog
proc create_dlg {wnd {p .}} {
unset -nocomplain ::dialog_ans
catch {destroy $wnd} ; # no error if it does not exist
toplevel $wnd -class Dialog
wm withdraw $wnd
if [winfo viewable $p] {wm transient $wnd $p}
if $::plain_unix {wm attributes $wnd -type dialog}
}
# Place a dialog centered wrt its parent.
# If its geometry is somehow not yet available,
# its upperleft corner will be centered.
proc place_dlg {wnd {p "."}} {
update idletasks
set g [wm geometry $p]
scan $g "%dx%d+%d+%d" pw ph px py
set hcenter [expr {$px + $pw / 2}]
set vcenter [expr {$py + $ph / 2}]
set g [wm geometry $wnd]
set wh [winfo reqheight $wnd]
set ww [winfo reqwidth $wnd]
set wx [expr {$hcenter - $ww / 2}]
if {$wx < 0} { set wx 0}
set wy [expr {$vcenter - $wh / 2}]
if {$wy < 0} { set wy 0}
wm geometry $wnd [format "+%d+%d" $wx $wy]
update idletasks
wm state $wnd normal
raise $wnd $p
tkwait visibility $wnd
focus $wnd
grab set $wnd
} ; # place_dlg
# in case pressing the closing button leads to lengthy processing:
proc disable_dlg {wnd} {
foreach c [winfo children $wnd] {
if {[winfo class $c] in $::active_cls} {
catch {$c state disabled}
}
}
}
proc end_dlg {ans wnd} {
set ::dialog_ans $ans
set p [winfo parent $wnd]
if {$p eq ""} {set p "."}
raise $p
destroy $wnd
} ; # end_dlg
# a possibly useful callback for WM_DELETE_WINDOW
proc cancel_or_destroy {ctrl topl} {
if [winfo exists $ctrl] {
$ctrl invoke
} elseif [winfo exists $topl] {
destroy $topl
}
}
##### directories #####
# slash flipping
proc forward_slashify {s} {
regsub -all {\\} $s {/} r
return $r
}
proc native_slashify {s} {
if {$::tcl_platform(platform) eq "windows"} {
regsub -all {/} $s {\\} r
} else {
regsub -all {\\} $s {/} r
}
return $r
}
# test whether a directory is writable.
# 'file writable' merely tests permissions, which may not be good enough
proc dir_writable {d} {
for {set x 0} {$x<100} {incr x} {
set y [expr {int(10000*rand())}]
set newfile [file join $d $y]
if [file exists $newfile] {
continue
} else {
if [catch {open $newfile w} fid] {
return 0
} else {
chan puts $fid "hello"
chan close $fid
if [file exists $newfile] {
file delete $newfile
return 1
} else {
return 0
}
}
}
}
return 0
}
# unix: choose_dir replacing native directory browser.
# the native FILE browser is ok, though.
if {$::tcl_platform(platform) eq "unix"} {
# Based on the directory browser from the tcl/tk widget demo.
# Also for MacOS, because we want to see /usr.
# For windows, the native browser widget is better.
## Code to populate a single directory node
proc populateTree {tree node} {
if {[$tree set $node type] ne "directory"} {
set type [$tree set $node type]
return
}
$tree delete [$tree children $node]
foreach f [lsort [glob -nocomplain -directory $node *]] {
set type [file type $f]
if {$type eq "directory"} {
$tree insert $node end \
-id $f -text [file tail $f] -values [list $type]
# Need at least one child to make this node openable,
# will be deleted when actually populating this node
$tree insert $f 0 -text "dummy"
}
}
# Stop this code from rerunning on the current node
$tree set $node type processedDirectory
}
proc choose_dir {initdir {parent .}} {
create_dlg .browser $parent
wm title .browser [__ "Browse..."]
# wallpaper
pack [ttk::frame .browser.bg -padding 3p] -fill both -expand 1
# ok and cancel buttons
pack [ttk::frame .browser.fr1] \
-in .browser.bg -side bottom -fill x
ppack [ttk::button .browser.ok -text [__ "Ok"]] \
-in .browser.fr1 -side right
ppack [ttk::button .browser.cancel -text [__ "Cancel"]] \
-in .browser.fr1 -side right
bind .browser <Escape> {.browser.cancel invoke}
wm protocol .browser WM_DELETE_WINDOW \
{cancel_or_destroy .browser.cancel .browser}
.browser.ok configure -command {
set ::dialog_ans [.browser.tree focus]
destroy .browser
}
.browser.cancel configure -command {
set ::dialog_ans ""
destroy .browser
}
## Create the tree and set it up
pack [ttk::frame .browser.fr0] -in .browser.bg -fill both -expand 1
set tree [ttk::treeview .browser.tree \
-columns {type} -displaycolumns {} -selectmode browse \
-yscroll ".browser.vsb set"]
.browser.tree column 0 -stretch 1
ttk::scrollbar .browser.vsb -orient vertical -command "$tree yview"
# hor. scrolling does not work, but toplevel and widget are resizable
$tree heading \#0 -text "/"
$tree insert {} end -id "/" -text "/" -values [list "directory"]
populateTree $tree "/"
bind $tree <<TreeviewOpen>> {
populateTree %W [%W focus]
}
bind $tree <ButtonRelease-1> {
.browser.tree heading \#0 -text [%W focus]
}
## Arrange the tree and its scrollbar in the toplevel
# Horizontal scrolling does not work, but resizing does.
grid $tree -in .browser.fr0 -row 0 -column 0 -sticky nsew
grid .browser.vsb -in .browser.fr0 -row 0 -column 1 -sticky ns
grid columnconfigure .browser.fr0 0 -weight 1
grid rowconfigure .browser.fr0 0 -weight 1
unset -nocomplain ::dialog_ans
# navigate tree to $initdir
set chosenDir {}
foreach d [file split [file normalize $initdir]] {
set nextdir [file join $chosenDir $d]
if [file isdirectory $nextdir] {
if {! [$tree exists $nextdir]} {
$tree insert $chosenDir end -id $nextdir \
-text $d -values [list "directory"]
}
populateTree $tree $nextdir
set chosenDir $nextdir
} else {
break
}
}
$tree see $chosenDir
$tree selection set [list $chosenDir]
$tree focus $chosenDir
$tree heading \#0 -text $chosenDir
place_dlg .browser $parent
tkwait window .browser
return $::dialog_ans
}; # choose_dir
}; # if unix
proc browse4dir {inidir {parent .}} {
if {$::tcl_platform(platform) eq "unix"} {
return [choose_dir $inidir $parent]
} else {
return [tk_chooseDirectory \
-initialdir $inidir -title [__ "Select or type"] -parent $parent]
}
} ; # browse4dir
# browse for a directory and store in entry- or label widget $w
proc dirbrowser2widget {w} {
set wclass [winfo class $w]
if {$wclass eq "Entry" || $wclass eq "TEntry"} {
set is_entry 1
} elseif {$wclass eq "Label" || $wclass eq "TLabel"} {
set is_entry 0
} else {
err_exit "browse2widget invoked with unsupported widget class $wclass"
}
if $is_entry {
set retval [$w get]
} else {
set retval [$w cget -text]
}
set retval [browse4dir $retval [winfo parent $w]]
if {$retval eq ""} {
return 0
} else {
if {$wclass eq "Entry" || $wclass eq "TEntry"} {
$w delete 0 end
$w insert 0 $retval
} else {
$w configure -text $retval
}
return 1
}
}
#### Unicode check- and radiobuttons ####
# on unix/linux the original indicators are hard-coded as bitmaps,
# which cannot scale with the rest of the interface.
# the hack below replaces them with unicode characters, which are scaled
# along with other text.
# This is implemented by removing the original indicators and prepending
# a unicode symbol and a unicode wide space to the text label.
# The combobox down arrow and the treeview triangles (directory browser)
# are scaled by normal style options at the end of redo_fonts.
if $::plain_unix {
# from General Punctuation, 2000-206f
set ::wsp \u2001 ; # wide space
# from Geometric Shapes, 25a0-25ff
set ::chk0 \u25a1
set ::chk1 \u25a3
set ::rad0 \u25cb
set ::rad1 \u25c9
# layouts copied from default theme, with indicator removed
ttk::style layout TCheckbutton "Checkbutton.padding -sticky nswe -children {Checkbutton.focus -side left -sticky w -children {Checkbutton.label -sticky nswe}}"
ttk::style layout TRadiobutton "Radiobutton.padding -sticky nswe -children {Radiobutton.focus -side left -sticky w -children {Radiobutton.label -sticky nswe}}"
proc tlupdate_check {w n e o} { ; # n, e, o added to keep trace happy
upvar [$w cget -variable] v
set s [$w cget -text]
set ck [expr {$v ? $::chk1 : $::chk0}]
set s0 [string index $s 0]
if {$s0 eq $::chk0 || $s0 eq $::chk1} {
set s "$ck$::wsp[string range $s 2 end]"
} else {
set s "$ck$::wsp$s"
}
if {[string length $s] == 2} {
# indicator plus wide space plus empty string. Remove wide space.
set s [string range $s 0 0]
}
$w configure -text $s
}
bind TCheckbutton <Map> {+tlupdate_check %W n e o}
bind TCheckbutton <Map> {+trace add variable [%W cget -variable] write \
[list tlupdate_check %W]}
bind TCheckbutton <Unmap> \
{+trace remove variable [%W cget -variable] write [list tlupdate_check %W]}
proc tlupdate_radio {w n e o} {
upvar [$w cget -variable] v
set ck [expr {$v eq [$w cget -value] ? $::rad1 : $::rad0}]
set s [$w cget -text]
set s0 [string index $s 0]
if {$s0 eq $::rad0 || $s0 eq $::rad1} {
set s "$ck$::wsp[string range $s 2 end]"
} else {
set s "$ck$::wsp$s"
}
if {[string length $s] == 2} {
# indicator plus wide space plus empty string. Remove wide space.
set s [string range $s 0 0]
}
$w configure -text $s
}
bind TRadiobutton <Map> {+tlupdate_radio %W n e o}
bind TRadiobutton <Map> {+trace add variable [%W cget -variable] write \
[list tlupdate_radio %W]}
bind TRadiobutton <Unmap> \
{+trace remove variable [%W cget -variable] write [list tlupdate_radio %W]}
}