{"thread":{"id":"11933","subject":"[PATCH v2] git-gui: Automatically spell check commit messages as the user types","startedAt":"2008-02-07T07:52:50Z","lastAt":"2008-02-07T07:52:50Z","messageCount":1,"participants":["Shawn O. Pearce"],"isPatch":true,"patchVersion":2,"patchTotal":null},"messages":[{"id":"67762","messageId":"20080207075250.GA24709@spearce.org","threadId":"11933","inReplyTo":null,"subject":"[PATCH v2] git-gui: Automatically spell check commit messages as the user types","fromName":"Shawn O. Pearce","fromEmail":"spearce@spearce.org","sentAt":"2008-02-07T07:52:50Z","receivedAt":"2008-02-07T07:52:50Z","isPatch":true,"sender":{"key":"spearce@spearce.org","avatar":"https://avatars.githubusercontent.com/u/34844?v=4"},"body":"This is likely going to be moved to my master branch in the next\nday or two.  I've been using it for a few days now and am pretty\nhappy with it.  Currently its parked in my pu branch.\n\nMajor changes since the first version are:\n\n  *) Options dialog allows dictionary selection;\n  *) We try to only spellcheck modified lines not everything;\n  *) It can be disabled by setting gui.spellingdictionary = none;\n\nI think there may still be a quirk with the suggestions popup.\nSometimes it doesn't show the suggestions for a word.\n\n--8>--\ngit-gui: Automatically spell check commit messages as the user types\n\nMany user friendly tools like word processors, email editors and web\nbrowsers allow users to spell check the message they are writing\nas they type it, making it easy to identify a common misspelling\nof a word and correct it on the fly.\n\nWe now open a bi-directional pipe to Aspell and feed the message\ntext the user is editing off to the program about once every 300\nmilliseconds.  This is frequent enough that the user sees the results\nalmost immediately, but is not so frequent as to cause significant\nadditional load on the system.  If the user has modified the message\ntext during the last 300 milliseconds we delay until the next period,\nensuring that we avoid flooding the Aspell process with a lot of\ntext while the user is actively typing their message.\n\nWe wait to send the current message buffer to Aspell until the user\nis at a word boundary, thus ensuring that we are not likely to ask\nfor misspelled word detection on a word that the user is actively\ntyping, as most words are misspelled when only partially typed,\neven if the user has thus far typed it correctly.\n\nMisspelled words are highlighted in red and are given an underline,\ncausing the word to stand out from the others in the buffer.  This is\na very common user interface idiom for displaying misspelled words,\nbut differs from one platform to the next in slight variations.\nFor example the Mac OS X system prefers using a dashed red underline,\nleaving the word in the original text color.  Unfortunately the\ncontrol that Tk gives us over text display is not powerful enough\nto handle such formatting so we have to work with the least common\ndenominator.\n\nThe top suggestions for a misspelling are saved in an array and\noffered to the user when they right-click (or on the Mac ctrl-click)\na misspelled word.  Selecting an entry from this menu will replace\nthe misspelling with the correction shown.  Replacement is integrated\nwith the undo/redo stack so undoing a replacement will restore the\nmisspelled original text.\n\nIf Aspell could not be started during git-gui launch we silently eat\nthe error and run without spell checking support.  This way users\nwho do not have Aspell in their $PATH can continue to use git-gui,\nalthough they will not get the advanced spelling functionality.\n\nIf Aspell started successfully the version line and language are\nshown in git-gui's about box, below the Tcl/Tk versions.  This way\nthe user can verify the Aspell function has been activated.\n\nIf Aspell crashes while we are running we inform the user with an\nerror dialog and then disable Aspell entirely for the rest of this\ngit-gui session.  This prevents us from fork-bombing the system\nwith Aspell instances that always crash when presented with the\ncurrent message text, should there be a bug in either Aspell or in\ngit-gui's output to it.\n\nWe escape all input lines with ^, as recommended by the Aspell manual\npage, as this allows Aspell to properly ignore any input line that is\notherwise looking like a command (e.g. ! to enable terse output).  By\nusing this escape however we need to correct all word offsets by -1 as\nAspell is apparently considering the ^ escape to be part of the line's\ncharacter count, but our Tk text widget obviously does not.\n\nAvailable dictionaries are offered in the Options dialog, allowing\nthe user to select the language they want to spellcheck commit\nmessages with for the current repository, as well as the global\nuser setting that all repositories inherit.\n\nSpecial thanks to Adam Flott for suggesting connecting git-gui\nto Aspell for the purpose of spell checking the commit message,\nand to Wincent Colaiuta for the idea to wait for a word boundary\nbefore passing the message over for checking.\n\nSigned-off-by: Shawn O. Pearce <spearce@spearce.org>\n---\n git-gui.sh         |   34 +++++-\n lib/about.tcl      |    5 +\n lib/option.tcl     |   41 ++++++\n lib/spellcheck.tcl |  363 ++++++++++++++++++++++++++++++++++++++++++++++++++++\n 4 files changed, 442 insertions(+), 1 deletions(-)\n create mode 100644 lib/spellcheck.tcl\n\ndiff --git a/git-gui.sh b/git-gui.sh\nindex f42e461..5d65272 100755\n--- a/git-gui.sh\n+++ b/git-gui.sh\n@@ -612,6 +612,7 @@ set default_config(gui.pruneduringfetch) false\n set default_config(gui.trustmtime) false\n set default_config(gui.diffcontext) 5\n set default_config(gui.newbranchtemplate) {}\n+set default_config(gui.spellingdictionary) {}\n set default_config(gui.fontui) [font configure font_ui]\n set default_config(gui.fontdiff) [font configure font_diff]\n set font_descs {\n@@ -1683,6 +1684,7 @@ set is_quitting 0\n proc do_quit {} {\n \tglobal ui_comm is_quitting repo_config commit_type\n \tglobal GITGUI_BCK_exists GITGUI_BCK_i\n+\tglobal ui_comm_spell\n \n \tif {$is_quitting} return\n \tset is_quitting 1\n@@ -1710,6 +1712,12 @@ proc do_quit {} {\n \t\t\t}\n \t\t}\n \n+\t\t# -- Cancel our spellchecker if its running.\n+\t\t#\n+\t\tif {[info exists ui_comm_spell]} {\n+\t\t\t$ui_comm_spell stop\n+\t\t}\n+\n \t\t# -- Remove our editor backup, its not needed.\n \t\t#\n \t\tafter cancel $GITGUI_BCK_i\n@@ -2454,7 +2462,7 @@ $ctxm add separator\n $ctxm add command \\\n \t-label [mc \"Sign Off\"] \\\n \t-command do_signoff\n-bind_button3 $ui_comm \"tk_popup $ctxm %X %Y\"\n+set ui_comm_ctxm $ctxm\n \n # -- Diff Header\n #\n@@ -2857,6 +2865,30 @@ if {[winfo exists $ui_comm]} {\n \t}\n \n \tbackup_commit_buffer\n+\n+\t# -- If the user has aspell available we can drive it\n+\t#    in pipe mode to spellcheck the commit message.\n+\t#\n+\tset spell_cmd [list |]\n+\tset spell_dict [get_config gui.spellingdictionary]\n+\tlappend spell_cmd aspell\n+\tif {$spell_dict ne {}} {\n+\t\tlappend spell_cmd --master=$spell_dict\n+\t}\n+\tlappend spell_cmd --mode=none\n+\tlappend spell_cmd --encoding=utf-8\n+\tlappend spell_cmd pipe\n+\tif {$spell_dict eq {none}\n+\t || [catch {set spell_fd [open $spell_cmd r+]} spell_err]} {\n+\t\tbind_button3 $ui_comm [list tk_popup $ui_comm_ctxm %X %Y]\n+\t} else {\n+\t\tset ui_comm_spell [spellcheck::init \\\n+\t\t\t$spell_fd \\\n+\t\t\t$ui_comm \\\n+\t\t\t$ui_comm_ctxm \\\n+\t\t]\n+\t}\n+\tunset -nocomplain spell_cmd spell_fd spell_err spell_dict\n }\n \n lock_index begin-read\ndiff --git a/lib/about.tcl b/lib/about.tcl\nindex 719fc54..47be8eb 100644\n--- a/lib/about.tcl\n+++ b/lib/about.tcl\n@@ -4,6 +4,7 @@\n proc do_about {} {\n \tglobal appvers copyright oguilib\n \tglobal tcl_patchLevel tk_patchLevel\n+\tglobal ui_comm_spell\n \n \tset w .about_dialog\n \ttoplevel $w\n@@ -40,6 +41,10 @@ proc do_about {} {\n \t\tappend v \"Tcl version $tcl_patchLevel\"\n \t\tappend v \", Tk version $tk_patchLevel\"\n \t}\n+\tif {[info exists ui_comm_spell]} {\n+\t\tappend v \"\\n\"\n+\t\tappend v [$ui_comm_spell version]\n+\t}\n \n \tset d {}\n \tappend d \"git wrapper: $::_git\\n\"\ndiff --git a/lib/option.tcl b/lib/option.tcl\nindex f812e5e..3bfa2ed 100644\n--- a/lib/option.tcl\n+++ b/lib/option.tcl\n@@ -5,6 +5,7 @@ proc save_config {} {\n \tglobal default_config font_descs\n \tglobal repo_config global_config\n \tglobal repo_config_new global_config_new\n+\tglobal ui_comm_spell\n \n \tforeach option $font_descs {\n \t\tset name [lindex $option 0]\n@@ -52,11 +53,23 @@ proc save_config {} {\n \t\t\tset repo_config($name) $value\n \t\t}\n \t}\n+\n+\tif {[info exists repo_config(gui.spellingdictionary)]} {\n+\t\tset value $repo_config(gui.spellingdictionary)\n+\t\tif {$value eq {none}} {\n+\t\t\tif {[info exists ui_comm_spell]} {\n+\t\t\t\t$ui_comm_spell stop\n+\t\t\t}\n+\t\t} elseif {[info exists ui_comm_spell]} {\n+\t\t\t$ui_comm_spell lang $value\n+\t\t}\n+\t}\n }\n \n proc do_options {} {\n \tglobal repo_config global_config font_descs\n \tglobal repo_config_new global_config_new\n+\tglobal ui_comm_spell\n \n \tarray unset repo_config_new\n \tarray unset global_config_new\n@@ -159,6 +172,34 @@ proc do_options {} {\n \t\t}\n \t}\n \n+\tset all_dicts [linsert \\\n+\t\t[spellcheck::available_langs] \\\n+\t\t0 \\\n+\t\tnone]\n+\tincr optid\n+\tforeach f {repo global} {\n+\t\tif {![info exists ${f}_config_new(gui.spellingdictionary)]} {\n+\t\t\tif {[info exists ui_comm_spell]} {\n+\t\t\t\tset value [$ui_comm_spell lang]\n+\t\t\t} else {\n+\t\t\t\tset value none\n+\t\t\t}\n+\t\t\tset ${f}_config_new(gui.spellingdictionary) $value\n+\t\t}\n+\n+\t\tframe $w.$f.$optid\n+\t\tlabel $w.$f.$optid.l -text [mc \"Spelling Dictionary:\"]\n+\t\teval tk_optionMenu $w.$f.$optid.v \\\n+\t\t\t${f}_config_new(gui.spellingdictionary) \\\n+\t\t\t$all_dicts\n+\t\tpack $w.$f.$optid.l -side left -anchor w -fill x\n+\t\tpack $w.$f.$optid.v -side left -anchor w \\\n+\t\t\t-fill x -expand 1 \\\n+\t\t\t-padx 5\n+\t\tpack $w.$f.$optid -side top -anchor w -fill x\n+\t}\n+\tunset all_dicts\n+\n \tset all_fonts [lsort [font families]]\n \tforeach option $font_descs {\n \t\tset name [lindex $option 0]\ndiff --git a/lib/spellcheck.tcl b/lib/spellcheck.tcl\nnew file mode 100644\nindex 0000000..01c2c4f\n--- /dev/null\n+++ b/lib/spellcheck.tcl\n@@ -0,0 +1,363 @@\n+# git-gui spellchecking support through aspell\n+# Copyright (C) 2008 Shawn Pearce\n+\n+class spellcheck {\n+\n+field s_fd     {} ; # pipe to aspell\n+field s_version   ; # aspell version string\n+field s_lang      ; # current language code\n+\n+field w_text      ; # text widget we are spelling\n+field w_menu      ; # context menu for the widget\n+field s_menuidx 0 ; # last index of insertion into $w_menu\n+\n+field s_i              ; # timer registration for _run callbacks\n+field s_clear        0 ; # did we erase mispelled tags yet?\n+field s_seen    [list] ; # lines last seen from $w_text in _run\n+field s_checked [list] ; # lines already checked\n+field s_pending [list] ; # [$line $data] sent to aspell\n+field s_suggest        ; # array, list of suggestions, keyed by misspelling\n+\n+constructor init {pipe_fd ui_text ui_menu} {\n+\tset w_text $ui_text\n+\tset w_menu $ui_menu\n+\n+\t_connect $this $pipe_fd\n+\treturn $this\n+}\n+\n+method _connect {pipe_fd} {\n+\tfconfigure $pipe_fd \\\n+\t\t-encoding utf-8 \\\n+\t\t-eofchar {} \\\n+\t\t-translation lf\n+\n+\tif {[gets $pipe_fd s_version] <= 0} {\n+\t\tclose $pipe_fd\n+\t\terror [mc \"Not connected to aspell\"]\n+\t}\n+\tif {{@(#) } ne [string range $s_version 0 4]} {\n+\t\tclose $pipe_fd\n+\t\terror [strcat [mc \"Unrecognized aspell version\"] \": $s_version\"]\n+\t}\n+\tset s_version [string range $s_version 5 end]\n+\n+\tputs $pipe_fd !             ; # enable terse mode\n+\tputs $pipe_fd {$$cr master} ; # fetch the language\n+\tflush $pipe_fd\n+\n+\tgets $pipe_fd s_lang\n+\tregexp {[/\\\\]([^/\\\\]+)\\.[^\\.]+$} $s_lang _ s_lang\n+\n+\tif {$::default_config(gui.spellingdictionary) eq {}\n+\t && [get_config gui.spellingdictionary] eq {}} {\n+\t\tset ::default_config(gui.spellingdictionary) $s_lang\n+\t}\n+\n+\tif {$s_fd ne {}} {\n+\t\tcatch {close $s_fd}\n+\t}\n+\tset s_fd $pipe_fd\n+\n+\tfconfigure $s_fd -blocking 0\n+\tfileevent $s_fd readable [cb _read]\n+\n+\t$w_text tag conf misspelled \\\n+\t\t-foreground red \\\n+\t\t-underline 1\n+\tbind_button3 $w_text [cb _popup_suggest %X %Y @%x,%y]\n+\n+\tarray unset s_suggest\n+\tset s_seen    [list]\n+\tset s_checked [list]\n+\tset s_pending [list]\n+\t_run $this\n+}\n+\n+method lang {{n {}}} {\n+\tif {$n ne {} && $s_lang ne $n} {\n+\t\tset spell_cmd [list |]\n+\t\tlappend spell_cmd aspell\n+\t\tlappend spell_cmd --master=$n\n+\t\tlappend spell_cmd --mode=none\n+\t\tlappend spell_cmd --encoding=UTF-8\n+\t\tlappend spell_cmd pipe\n+\t\t_connect $this [open $spell_cmd r+]\n+\t}\n+\treturn $s_lang\n+}\n+\n+method version {} {\n+\treturn \"$s_version, $s_lang\"\n+}\n+\n+method stop {} {\n+\twhile {$s_menuidx > 0} {\n+\t\t$w_menu delete 0\n+\t\tincr s_menuidx -1\n+\t}\n+\t$w_text tag delete misspelled\n+\n+\tcatch {close $s_fd}\n+\tcatch {after cancel $s_i}\n+\tset s_fd {}\n+\tset s_i {}\n+\tset s_lang {}\n+}\n+\n+method _popup_suggest {X Y pos} {\n+\twhile {$s_menuidx > 0} {\n+\t\t$w_menu delete 0\n+\t\tincr s_menuidx -1\n+\t}\n+\n+\tset b_loc [$w_text index \"$pos wordstart\"]\n+\tset e_loc [_wordend $this $b_loc]\n+\tset orig  [$w_text get $b_loc $e_loc]\n+\tset tags  [$w_text tag names $b_loc]\n+\n+\tif {[lsearch -exact $tags misspelled] >= 0} {\n+\t\tif {[info exists s_suggest($orig)]} {\n+\t\t\tset cnt 0\n+\t\t\tforeach s $s_suggest($orig) {\n+\t\t\t\tif {$cnt < 5} {\n+\t\t\t\t\t$w_menu insert $s_menuidx command \\\n+\t\t\t\t\t\t-label $s \\\n+\t\t\t\t\t\t-command [cb _replace $b_loc $e_loc $s]\n+\t\t\t\t\tincr s_menuidx\n+\t\t\t\t\tincr cnt\n+\t\t\t\t} else {\n+\t\t\t\t\tbreak\n+\t\t\t\t}\n+\t\t\t}\n+\t\t} else {\n+\t\t\t$w_menu insert $s_menuidx command \\\n+\t\t\t\t-label [mc \"No Suggestions\"] \\\n+\t\t\t\t-state disabled\n+\t\t\tincr s_menuidx\n+\t\t}\n+\t\t$w_menu insert $s_menuidx separator\n+\t\tincr s_menuidx\n+\t}\n+\n+\t$w_text mark set saved-insert insert\n+\ttk_popup $w_menu $X $Y\n+}\n+\n+method _replace {b_loc e_loc word} {\n+\t$w_text configure -autoseparators 0\n+\t$w_text edit separator\n+\n+\t$w_text delete $b_loc $e_loc\n+\t$w_text insert $b_loc $word\n+\n+\t$w_text edit separator\n+\t$w_text configure -autoseparators 1\n+\t$w_text mark set insert saved-insert\n+}\n+\n+method _restart_timer {} {\n+\tset s_i [after 300 [cb _run]]\n+}\n+\n+proc _match_length {max_line arr_name} {\n+\tupvar $arr_name a\n+\n+\tif {[llength $a] > $max_line} {\n+\t\tset a [lrange $a 0 $max_line]\n+\t}\n+\twhile {[llength $a] <= $max_line} {\n+\t\tlappend a {}\n+\t}\n+}\n+\n+method _wordend {pos} {\n+\tset pos  [$w_text index \"$pos wordend\"]\n+\tset tags [$w_text tag names $pos]\n+\twhile {[lsearch -exact $tags misspelled] >= 0} {\n+\t\tset pos  [$w_text index \"$pos +1c\"]\n+\t\tset tags [$w_text tag names $pos]\n+\t}\n+\treturn $pos\n+}\n+\n+method _run {} {\n+\tset cur_pos  [$w_text index {insert -1c}]\n+\tset cur_line [lindex [split $cur_pos .] 0]\n+\tset max_line [lindex [split [$w_text index end] .] 0]\n+\t_match_length $max_line s_seen\n+\t_match_length $max_line s_checked\n+\n+\t# Nothing in the message buffer?  Nothing to spellcheck.\n+\t#\n+\tif {$cur_line == 1\n+\t && $max_line == 2\n+\t && [$w_text get 1.0 end] eq \"\\n\"} {\n+\t\tarray unset s_suggest\n+\t\t_restart_timer $this\n+\t\treturn\n+\t}\n+\n+\tset active 0\n+\tfor {set n 1} {$n <= $max_line} {incr n} {\n+\t\tset s [$w_text get \"$n.0\" \"$n.end\"]\n+\n+\t\t# Don't spellcheck the current line unless we are at\n+\t\t# a word boundary.  The user might be typing on it.\n+\t\t#\n+\t\tif {$n == $cur_line\n+\t\t && ![regexp {^\\W$} [$w_text get $cur_pos insert]]} {\n+\n+\t\t\t# If the current word is mispelled remove the tag\n+\t\t\t# but force a spellcheck later.\n+\t\t\t#\n+\t\t\tset tags [$w_text tag names $cur_pos]\n+\t\t\tif {[lsearch -exact $tags misspelled] >= 0} {\n+\t\t\t\t$w_text tag remove misspelled \\\n+\t\t\t\t\t\"$cur_pos wordstart\" \\\n+\t\t\t\t\t[_wordend $this $cur_pos]\n+\t\t\t\tlset s_seen    $n $s\n+\t\t\t\tlset s_checked $n {}\n+\t\t\t}\n+\n+\t\t\tcontinue\n+\t\t}\n+\n+\t\tif {[lindex $s_seen    $n] eq $s\n+\t\t && [lindex $s_checked $n] ne $s} {\n+\t\t\t# Don't send empty lines to Aspell it doesn't check them.\n+\t\t\t#\n+\t\t\tif {$s eq {}} {\n+\t\t\t\tlset s_checked $n $s\n+\t\t\t\tcontinue\n+\t\t\t}\n+\n+\t\t\t# Don't send typical s-b-o lines as the emails are\n+\t\t\t# almost always misspelled according to Aspell.\n+\t\t\t#\n+\t\t\tif {[regexp -nocase {^[a-z-]+-by:.*<.*@.*>$} $s]} {\n+\t\t\t\t$w_text tag remove misspelled \"$n.0\" \"$n.end\"\n+\t\t\t\tlset s_checked $n $s\n+\t\t\t\tcontinue\n+\t\t\t}\n+\n+\t\t\tputs $s_fd ^$s\n+\t\t\tlappend s_pending [list $n $s]\n+\t\t\tset active 1\n+\t\t} else {\n+\t\t\t# Delay until another idle loop to make sure we don't\n+\t\t\t# spellcheck lines the user is actively changing.\n+\t\t\t#\n+\t\t\tlset s_seen $n $s\n+\t\t}\n+\t}\n+\n+\tif {$active} {\n+\t\tset s_clear 1\n+\t\tflush $s_fd\n+\t} else {\n+\t\t_restart_timer $this\n+\t}\n+}\n+\n+method _read {} {\n+\twhile {[gets $s_fd line] >= 0} {\n+\t\tset lineno [lindex $s_pending 0 0]\n+\n+\t\tif {$s_clear} {\n+\t\t\t$w_text tag remove misspelled \"$lineno.0\" \"$lineno.end\"\n+\t\t\tset s_clear 0\n+\t\t}\n+\n+\t\tif {$line eq {}} {\n+\t\t\tlset s_checked $lineno [lindex $s_pending 0 1]\n+\t\t\tset s_pending [lrange $s_pending 1 end]\n+\t\t\tset s_clear 1\n+\t\t\tcontinue\n+\t\t}\n+\n+\t\tset sugg [list]\n+\t\tswitch -- [string range $line 0 1] {\n+\t\t{& } {\n+\t\t\tset line [split [string range $line 2 end] :]\n+\t\t\tset info [split [lindex $line 0] { }]\n+\t\t\tset orig [lindex $info 0]\n+\t\t\tset offs [lindex $info 2]\n+\t\t\tforeach s [split [lindex $line 1] ,] {\n+\t\t\t\tlappend sugg [string range $s 1 end]\n+\t\t\t}\n+\t\t}\n+\t\t{# } {\n+\t\t\tset info [split [string range $line 2 end] { }]\n+\t\t\tset orig [lindex $info 0]\n+\t\t\tset offs [lindex $info 1]\n+\t\t}\n+\t\tdefault {\n+\t\t\tputs stderr \"<spell> $line\"\n+\t\t\tcontinue\n+\t\t}\n+\t\t}\n+\n+\t\tincr offs -1\n+\t\tset b_loc \"$lineno.$offs\"\n+\t\tset e_loc [$w_text index \"$lineno.$offs wordend\"]\n+\t\tset curr [$w_text get $b_loc $e_loc]\n+\n+\t\t# At least for English curr = \"bob\", orig = \"bob's\"\n+\t\t# so Tk didn't include the 's but Aspell did.  We\n+\t\t# try to round out the word.\n+\t\t#\n+\t\twhile {$curr ne $orig\n+\t\t && [string equal -length [llength $curr] $curr $orig]} {\n+\t\t\tset n_loc  [$w_text index \"$e_loc +1c\"]\n+\t\t\tset n_curr [$w_text get $b_loc $n_loc]\n+\t\t\tif {$n_curr eq $curr} {\n+\t\t\t\tbreak\n+\t\t\t}\n+\t\t\tset curr  $n_curr\n+\t\t\tset e_loc $n_loc\n+\t\t}\n+\n+\t\tif {$curr eq $orig} {\n+\t\t\t$w_text tag add misspelled $b_loc $e_loc\n+\t\t\tif {[llength $sugg] > 0} {\n+\t\t\t\tset s_suggest($orig) $sugg\n+\t\t\t} else {\n+\t\t\t\tunset -nocomplain s_suggest($orig)\n+\t\t\t}\n+\t\t} else {\n+\t\t\tunset -nocomplain s_suggest($orig)\n+\t\t}\n+\t}\n+\n+\tfconfigure $s_fd -block 1\n+\tif {[eof $s_fd]} {\n+\t\tif {![catch {close $s_fd} err]} {\n+\t\t\tset err [mc \"unexpected eof from aspell\"]\n+\t\t}\n+\t\tcatch {after cancel $s_i}\n+\t\t$w_text tag remove misspelled 1.0 end\n+\t\terror_popup [strcat \"Spell Checker Failed\" \"\\n\\n\" $err]\n+\t\treturn\n+\t}\n+\tfconfigure $s_fd -block 0\n+\n+\tif {[llength $s_pending] == 0} {\n+\t\t_restart_timer $this\n+\t}\n+}\n+\n+proc available_langs {} {\n+\tset langs [list]\n+\tcatch {\n+\t\tset fd [open [list | aspell dump dicts] r]\n+\t\twhile {[gets $fd line] >= 0} {\n+\t\t\tif {$line eq {}} continue\n+\t\t\tlappend langs $line\n+\t\t}\n+\t\tclose $fd\n+\t}\n+\treturn $langs\n+}\n+\n+}\n-- \n1.5.4.1213.g2bdeb\n"}]}