# A Tk console widget for SQLite. Invoke sqlitecon::create with a window name, # a prompt string, a title to set a new top-level window, and the SQLite # database handle. For example: # # sqlitecon::create .sqlcon {sql:- } {SQL Console} db # # A toplevel window is created that allows you to type in SQL commands to # be processed on the spot. # # A limited set of dot-commands are supported: # # .table # .schema ?TABLE? # .mode list|column|multicolumn|line # .exit # # In addition, a new SQL function named "edit()" is created. This function # takes a single text argument and returns a text result. Whenever the # the function is called, it pops up a new toplevel window containing a # text editor screen initialized to the argument. When the "OK" button # is pressed, whatever revised text is in the text editor is returned as # the result of the edit() function. This allows text fields of SQL tables # to be edited quickly and easily as follows: # # UPDATE table1 SET dscr = edit(dscr) WHERE rowid=15; # # Create a namespace to work in # namespace eval ::sqlitecon { # do nothing } # Create a console widget named $w. The prompt string is $prompt. # The title at the top of the window is $title. The database connection # object is $db # proc sqlitecon::create {w prompt title db} { upvar #0 $w.t v if {[winfo exists $w]} {destroy $w} if {[info exists v]} {unset v} toplevel $w wm title $w $title wm iconname $w $title frame $w.mb -bd 2 -relief raised pack $w.mb -side top -fill x menubutton $w.mb.file -text File -menu $w.mb.file.m menubutton $w.mb.edit -text Edit -menu $w.mb.edit.m pack $w.mb.file $w.mb.edit -side left -padx 8 -pady 1 set m [menu $w.mb.file.m -tearoff 0] $m add command -label {Close} -command "destroy $w" sqlitecon::create_child $w $prompt $w.mb.edit.m set v(db) $db $db function edit ::sqlitecon::_edit } # This routine creates a console as a child window within a larger # window. It also creates an edit menu named "$editmenu" if $editmenu!="". # The calling function is responsible for posting the edit menu. # proc sqlitecon::create_child {w prompt editmenu} { upvar #0 $w.t v if {$editmenu!=""} { set m [menu $editmenu -tearoff 0] $m add command -label Cut -command "sqlitecon::Cut $w.t" $m add command -label Copy -command "sqlitecon::Copy $w.t" $m add command -label Paste -command "sqlitecon::Paste $w.t" $m add command -label {Clear Screen} -command "sqlitecon::Clear $w.t" $m add separator $m add command -label {Save As...} -command "sqlitecon::SaveFile $w.t" catch {$editmenu config -postcommand "sqlitecon::EnableEditMenu $w"} } scrollbar $w.sb -orient vertical -command "$w.t yview" pack $w.sb -side right -fill y text $w.t -font fixed -yscrollcommand "$w.sb set" pack $w.t -side right -fill both -expand 1 bindtags $w.t Sqlitecon set v(editmenu) $editmenu set v(history) 0 set v(historycnt) 0 set v(current) -1 set v(prompt) $prompt set v(prior) {} set v(plength) [string length $v(prompt)] set v(x) 0 set v(y) 0 set v(mode) column set v(header) on $w.t mark set insert end $w.t tag config ok -foreground blue $w.t tag config err -foreground red $w.t insert end $v(prompt) $w.t mark set out 1.0 after idle "focus $w.t" } bind Sqlitecon <1> {sqlitecon::Button1 %W %x %y} bind Sqlitecon <B1-Motion> {sqlitecon::B1Motion %W %x %y} bind Sqlitecon <B1-Leave> {sqlitecon::B1Leave %W %x %y} bind Sqlitecon <B1-Enter> {sqlitecon::cancelMotor %W} bind Sqlitecon <ButtonRelease-1> {sqlitecon::cancelMotor %W} bind Sqlitecon <KeyPress> {sqlitecon::Insert %W %A} bind Sqlitecon <Left> {sqlitecon::Left %W} bind Sqlitecon <Control-b> {sqlitecon::Left %W} bind Sqlitecon <Right> {sqlitecon::Right %W} bind Sqlitecon <Control-f> {sqlitecon::Right %W} bind Sqlitecon <BackSpace> {sqlitecon::Backspace %W} bind Sqlitecon <Control-h> {sqlitecon::Backspace %W} bind Sqlitecon <Delete> {sqlitecon::Delete %W} bind Sqlitecon <Control-d> {sqlitecon::Delete %W} bind Sqlitecon <Home> {sqlitecon::Home %W} bind Sqlitecon <Control-a> {sqlitecon::Home %W} bind Sqlitecon <End> {sqlitecon::End %W} bind Sqlitecon <Control-e> {sqlitecon::End %W} bind Sqlitecon <Return> {sqlitecon::Enter %W} bind Sqlitecon <KP_Enter> {sqlitecon::Enter %W} bind Sqlitecon <Up> {sqlitecon::Prior %W} bind Sqlitecon <Control-p> {sqlitecon::Prior %W} bind Sqlitecon <Down> {sqlitecon::Next %W} bind Sqlitecon <Control-n> {sqlitecon::Next %W} bind Sqlitecon <Control-k> {sqlitecon::EraseEOL %W} bind Sqlitecon <<Cut>> {sqlitecon::Cut %W} bind Sqlitecon <<Copy>> {sqlitecon::Copy %W} bind Sqlitecon <<Paste>> {sqlitecon::Paste %W} bind Sqlitecon <<Clear>> {sqlitecon::Clear %W} # Insert a single character at the insertion cursor # proc sqlitecon::Insert {w a} { $w insert insert $a $w yview insert } # Move the cursor one character to the left # proc sqlitecon::Left {w} { upvar #0 $w v scan [$w index insert] %d.%d row col if {$col>$v(plength)} { $w mark set insert "insert -1c" } } # Erase the character to the left of the cursor # proc sqlitecon::Backspace {w} { upvar #0 $w v scan [$w index insert] %d.%d row col if {$col>$v(plength)} { $w delete {insert -1c} } } # Erase to the end of the line # proc sqlitecon::EraseEOL {w} { upvar #0 $w v scan [$w index insert] %d.%d row col if {$col>=$v(plength)} { $w delete insert {insert lineend} } } # Move the cursor one character to the right # proc sqlitecon::Right {w} { $w mark set insert "insert +1c" } # Erase the character to the right of the cursor # proc sqlitecon::Delete w { $w delete insert } # Move the cursor to the beginning of the current line # proc sqlitecon::Home w { upvar #0 $w v scan [$w index insert] %d.%d row col $w mark set insert $row.$v(plength) } # Move the cursor to the end of the current line # proc sqlitecon::End w { $w mark set insert {insert lineend} } # Add a line to the history # proc sqlitecon::addHistory {w line} { upvar #0 $w v if {$v(historycnt)>0} { set last [lindex $v(history) [expr $v(historycnt)-1]] if {[string compare $last $line]} { lappend v(history) $line incr v(historycnt) } } else { set v(history) [list $line] set v(historycnt) 1 } set v(current) $v(historycnt) } # Called when "Enter" is pressed. Do something with the line # of text that was entered. # proc sqlitecon::Enter w { upvar #0 $w v scan [$w index insert] %d.%d row col set start $row.$v(plength) set line [$w get $start "$start lineend"] $w insert end \n $w mark set out end if {$v(prior)==""} { set cmd $line } else { set cmd $v(prior)\n$line } if {[string index $cmd 0]=="." || [$v(db) complete $cmd]} { regsub -all {\n} [string trim $cmd] { } cmd2 addHistory $w $cmd2 set rc [catch {DoCommand $w $cmd} res] if {![winfo exists $w]} return if {$rc} { $w insert end $res\n err } elseif {[string length $res]>0} { $w insert end $res\n ok } set v(prior) {} $w insert end $v(prompt) } else { set v(prior) $cmd regsub -all {[^ ]} $v(prompt) . x $w insert end $x } $w mark set insert end $w mark set out {insert linestart} $w yview insert } # Execute a single SQL command. Pay special attention to control # directives that begin with "." # # The return value is the text output from the command, properly # formatted. # proc sqlitecon::DoCommand {w cmd} { upvar #0 $w v set mode $v(mode) set header $v(header) if {[regexp {^(\.[a-z]+)} $cmd all word]} { if {$word==".mode"} { regexp {^.[a-z]+ +([a-z]+)} $cmd all v(mode) return {} } elseif {$word==".exit"} { destroy [winfo toplevel $w] return {} } elseif {$word==".header"} { regexp {^.[a-z]+ +([a-z]+)} $cmd all v(header) return {} } elseif {$word==".tables"} { set mode multicolumn set cmd {SELECT name FROM sqlite_master WHERE type='table' UNION ALL SELECT name FROM sqlite_temp_master WHERE type='table'} $v(db) eval {PRAGMA database_list} { if {$name!="temp" && $name!="main"} { append cmd "UNION ALL SELECT name FROM $name.sqlite_master\ WHERE type='table'" } } append cmd { ORDER BY 1} } elseif {$word==".fullschema"} { set pattern % regexp {^.[a-z]+ +([^ ]+)} $cmd all pattern set mode list set header 0 set cmd "SELECT sql FROM sqlite_master WHERE tbl_name LIKE '$pattern' AND sql NOT NULL UNION ALL SELECT sql FROM sqlite_temp_master WHERE tbl_name LIKE '$pattern' AND sql NOT NULL" $v(db) eval {PRAGMA database_list} { if {$name!="temp" && $name!="main"} { append cmd " UNION ALL SELECT sql FROM $name.sqlite_master\ WHERE tbl_name LIKE '$pattern' AND sql NOT NULL" } } } elseif {$word==".schema"} { set pattern % regexp {^.[a-z]+ +([^ ]+)} $cmd all pattern set mode list set header 0 set cmd "SELECT sql FROM sqlite_master WHERE name LIKE '$pattern' AND sql NOT NULL UNION ALL SELECT sql FROM sqlite_temp_master WHERE name LIKE '$pattern' AND sql NOT NULL" $v(db) eval {PRAGMA database_list} { if {$name!="temp" && $name!="main"} { append cmd " UNION ALL SELECT sql FROM $name.sqlite_master\ WHERE name LIKE '$pattern' AND sql NOT NULL" } } } else { return \ ".exit\n.mode line|list|column\n.schema ?TABLENAME?\n.tables" } } set res {} if {$mode=="list"} { $v(db) eval $cmd x { set sep {} foreach col $x(*) { append res $sep$x($col) set sep | } append res \n } if {[info exists x(*)] && $header} { set sep {} set hdr {} foreach col $x(*) { append hdr $sep$col set sep | } set res $hdr\n$res } } elseif {[string range $mode 0 2]=="col"} { set y {} $v(db) eval $cmd x { foreach col $x(*) { if {![info exists cw($col)] || $cw($col)<[string length $x($col)]} { set cw($col) [string length $x($col)] } lappend y $x($col) } } if {[info exists x(*)] && $header} { set hdr {} set ln {} set dash --------------------------------------------------------------- append dash ------------------------------------------------------------ foreach col $x(*) { if {![info exists cw($col)] || $cw($col)<[string length $col]} { set cw($col) [string length $col] } lappend hdr $col lappend ln [string range $dash 1 $cw($col)] } set y [concat $hdr $ln $y] } if {[info exists x(*)]} { set format {} set arglist {} set arglist2 {} set i 0 foreach col $x(*) { lappend arglist x$i append arglist2 " \$x$i" incr i append format " %-$cw($col)s" } set format [string trimleft $format]\n if {[llength $arglist]>0} { foreach $arglist $y "append res \[format [list $format] $arglist2\]" } } } elseif {$mode=="multicolumn"} { set y [$v(db) eval $cmd] set max 0 foreach e $y { if {$max<[string length $e]} {set max [string length $e]} } set ncol [expr {int(80/($max+2))}] if {$ncol<1} {set ncol 1} set nelem [llength $y] set nrow [expr {($nelem+$ncol-1)/$ncol}] set format "%-${max}s" for {set i 0} {$i<$nrow} {incr i} { set j $i while 1 { append res [format $format [lindex $y $j]] incr j $nrow if {$j>=$nelem} break append res { } } append res \n } } else { $v(db) eval $cmd x { foreach col $x(*) {append res "$col = $x($col)\n"} append res \n } } return [string trimright $res] } # Change the line to the previous line # proc sqlitecon::Prior w { upvar #0 $w v if {$v(current)<=0} return incr v(current) -1 set line [lindex $v(history) $v(current)] sqlitecon::SetLine $w $line } # Change the line to the next line # proc sqlitecon::Next w { upvar #0 $w v if {$v(current)>=$v(historycnt)} return incr v(current) 1 set line [lindex $v(history) $v(current)] sqlitecon::SetLine $w $line } # Change the contents of the entry line # proc sqlitecon::SetLine {w line} { upvar #0 $w v scan [$w index insert] %d.%d row col set start $row.$v(plength) $w delete $start end $w insert end $line $w mark set insert end $w yview insert } # Called when the mouse button is pressed at position $x,$y on # the console widget. # proc sqlitecon::Button1 {w x y} { global tkPriv upvar #0 $w v set v(mouseMoved) 0 set v(pressX) $x set p [sqlitecon::nearestBoundry $w $x $y] scan [$w index insert] %d.%d ix iy scan $p %d.%d px py if {$px==$ix} { $w mark set insert $p } $w mark set anchor $p focus $w } # Find the boundry between characters that is nearest # to $x,$y # proc sqlitecon::nearestBoundry {w x y} { set p [$w index @$x,$y] set bb [$w bbox $p] if {![string compare $bb ""]} {return $p} if {($x-[lindex $bb 0])<([lindex $bb 2]/2)} {return $p} $w index "$p + 1 char" } # This routine extends the selection to the point specified by $x,$y # proc sqlitecon::SelectTo {w x y} { upvar #0 $w v set cur [sqlitecon::nearestBoundry $w $x $y] if {[catch {$w index anchor}]} { $w mark set anchor $cur } set anchor [$w index anchor] if {[$w compare $cur != $anchor] || (abs($v(pressX) - $x) >= 3)} { if {$v(mouseMoved)==0} { $w tag remove sel 0.0 end } set v(mouseMoved) 1 } if {[$w compare $cur < anchor]} { set first $cur set last anchor } else { set first anchor set last $cur } if {$v(mouseMoved)} { $w tag remove sel 0.0 $first $w tag add sel $first $last $w tag remove sel $last end update idletasks } } # Called whenever the mouse moves while button-1 is held down. # proc sqlitecon::B1Motion {w x y} { upvar #0 $w v set v(y) $y set v(x) $x sqlitecon::SelectTo $w $x $y } # Called whenever the mouse leaves the boundries of the widget # while button 1 is held down. # proc sqlitecon::B1Leave {w x y} { upvar #0 $w v set v(y) $y set v(x) $x sqlitecon::motor $w } # This routine is called to automatically scroll the window when # the mouse drags offscreen. # proc sqlitecon::motor w { upvar #0 $w v if {![winfo exists $w]} return if {$v(y)>=[winfo height $w]} { $w yview scroll 1 units } elseif {$v(y)<0} { $w yview scroll -1 units } else { return } sqlitecon::SelectTo $w $v(x) $v(y) set v(timer) [after 50 sqlitecon::motor $w] } # This routine cancels the scrolling motor if it is active # proc sqlitecon::cancelMotor w { upvar #0 $w v catch {after cancel $v(timer)} catch {unset v(timer)} } # Do a Copy operation on the stuff currently selected. # proc sqlitecon::Copy w { if {![catch {set text [$w get sel.first sel.last]}]} { clipboard clear -displayof $w clipboard append -displayof $w $text } } # Return 1 if the selection exists and is contained # entirely on the input line. Return 2 if the selection # exists but is not entirely on the input line. Return 0 # if the selection does not exist. # proc sqlitecon::canCut w { set r [catch { scan [$w index sel.first] %d.%d s1x s1y scan [$w index sel.last] %d.%d s2x s2y scan [$w index insert] %d.%d ix iy }] if {$r==1} {return 0} if {$s1x==$ix && $s2x==$ix} {return 1} return 2 } # Do a Cut operation if possible. Cuts are only allowed # if the current selection is entirely contained on the # current input line. # proc sqlitecon::Cut w { if {[sqlitecon::canCut $w]==1} { sqlitecon::Copy $w $w delete sel.first sel.last } } # Do a paste opeation. # proc sqlitecon::Paste w { if {[sqlitecon::canCut $w]==1} { $w delete sel.first sel.last } if {[catch {selection get -displayof $w -selection CLIPBOARD} topaste] && [catch {selection get -displayof $w -selection PRIMARY} topaste]} { return } if {[info exists ::$w]} { set prior 0 foreach line [split $topaste \n] { if {$prior} { sqlitecon::Enter $w update } set prior 1 $w insert insert $line } } else { $w insert insert $topaste } } # Enable or disable entries in the Edit menu # proc sqlitecon::EnableEditMenu w { upvar #0 $w.t v set m $v(editmenu) if {$m=="" || ![winfo exists $m]} return switch [sqlitecon::canCut $w.t] { 0 { $m entryconf Copy -state disabled $m entryconf Cut -state disabled } 1 { $m entryconf Copy -state normal $m entryconf Cut -state normal } 2 { $m entryconf Copy -state normal $m entryconf Cut -state disabled } } } # Prompt the user for the name of a writable file. Then write the # entire contents of the console screen to that file. # proc sqlitecon::SaveFile w { set types { {{Text Files} {.txt}} {{All Files} *} } set f [tk_getSaveFile -filetypes $types -title "Write Screen To..."] if {$f!=""} { if {[catch {open $f w} fd]} { tk_messageBox -type ok -icon error -message $fd } else { puts $fd [string trimright [$w get 1.0 end] \n] close $fd } } } # Erase everything from the console above the insertion line. # proc sqlitecon::Clear w { $w delete 1.0 {insert linestart} } # An in-line editor for SQL # proc sqlitecon::_edit {origtxt {title {}}} { for {set i 0} {[winfo exists .ed$i]} {incr i} continue set w .ed$i toplevel $w wm protocol $w WM_DELETE_WINDOW "$w.b.can invoke" wm title $w {Inline SQL Editor} frame $w.b pack $w.b -side bottom -fill x button $w.b.can -text Cancel -width 6 -command [list set ::$w 0] button $w.b.ok -text OK -width 6 -command [list set ::$w 1] button $w.b.cut -text Cut -width 6 -command [list ::sqlitecon::Cut $w.t] button $w.b.copy -text Copy -width 6 -command [list ::sqlitecon::Copy $w.t] button $w.b.paste -text Paste -width 6 -command [list ::sqlitecon::Paste $w.t] set ::$w {} pack $w.b.cut $w.b.copy $w.b.paste $w.b.can $w.b.ok\ -side left -padx 5 -pady 5 -expand 1 if {$title!=""} { label $w.title -text $title pack $w.title -side top -padx 5 -pady 5 } text $w.t -bg white -fg black -yscrollcommand [list $w.sb set] pack $w.t -side left -fill both -expand 1 scrollbar $w.sb -orient vertical -command [list $w.t yview] pack $w.sb -side left -fill y $w.t insert end $origtxt vwait ::$w if {[set ::$w]} { set txt [string trimright [$w.t get 1.0 end]] } else { set txt $origtxt } destroy $w return $txt }