1 # A Tk console widget for SQLite. Invoke sqlitecon::create with a window name, 2 # a prompt string, a title to set a new top-level window, and the SQLite 3 # database handle. For example: 4 # 5 # sqlitecon::create .sqlcon {sql:- } {SQL Console} db 6 # 7 # A toplevel window is created that allows you to type in SQL commands to 8 # be processed on the spot. 9 # 10 # A limited set of dot-commands are supported: 11 # 12 # .table 13 # .schema ?TABLE? 14 # .mode list|column|multicolumn|line 15 # .exit 16 # 17 # In addition, a new SQL function named "edit()" is created. This function 18 # takes a single text argument and returns a text result. Whenever the 19 # the function is called, it pops up a new toplevel window containing a 20 # text editor screen initialized to the argument. When the "OK" button 21 # is pressed, whatever revised text is in the text editor is returned as 22 # the result of the edit() function. This allows text fields of SQL tables 23 # to be edited quickly and easily as follows: 24 # 25 # UPDATE table1 SET dscr = edit(dscr) WHERE rowid=15; 26 # 27 28 29 # Create a namespace to work in 30 # 31 namespace eval ::sqlitecon { 32 # do nothing 33 } 34 35 # Create a console widget named $w. The prompt string is $prompt. 36 # The title at the top of the window is $title. The database connection 37 # object is $db 38 # 39 proc sqlitecon::create {w prompt title db} { 40 upvar #0 $w.t v 41 if {[winfo exists $w]} {destroy $w} 42 if {[info exists v]} {unset v} 43 toplevel $w 44 wm title $w $title 45 wm iconname $w $title 46 frame $w.mb -bd 2 -relief raised 47 pack $w.mb -side top -fill x 48 menubutton $w.mb.file -text File -menu $w.mb.file.m 49 menubutton $w.mb.edit -text Edit -menu $w.mb.edit.m 50 pack $w.mb.file $w.mb.edit -side left -padx 8 -pady 1 51 set m [menu $w.mb.file.m -tearoff 0] 52 $m add command -label {Close} -command "destroy $w" 53 sqlitecon::create_child $w $prompt $w.mb.edit.m 54 set v(db) $db 55 $db function edit ::sqlitecon::_edit 56 } 57 58 # This routine creates a console as a child window within a larger 59 # window. It also creates an edit menu named "$editmenu" if $editmenu!="". 60 # The calling function is responsible for posting the edit menu. 61 # 62 proc sqlitecon::create_child {w prompt editmenu} { 63 upvar #0 $w.t v 64 if {$editmenu!=""} { 65 set m [menu $editmenu -tearoff 0] 66 $m add command -label Cut -command "sqlitecon::Cut $w.t" 67 $m add command -label Copy -command "sqlitecon::Copy $w.t" 68 $m add command -label Paste -command "sqlitecon::Paste $w.t" 69 $m add command -label {Clear Screen} -command "sqlitecon::Clear $w.t" 70 $m add separator 71 $m add command -label {Save As...} -command "sqlitecon::SaveFile $w.t" 72 catch {$editmenu config -postcommand "sqlitecon::EnableEditMenu $w"} 73 } 74 scrollbar $w.sb -orient vertical -command "$w.t yview" 75 pack $w.sb -side right -fill y 76 text $w.t -font fixed -yscrollcommand "$w.sb set" 77 pack $w.t -side right -fill both -expand 1 78 bindtags $w.t Sqlitecon 79 set v(editmenu) $editmenu 80 set v(history) 0 81 set v(historycnt) 0 82 set v(current) -1 83 set v(prompt) $prompt 84 set v(prior) {} 85 set v(plength) [string length $v(prompt)] 86 set v(x) 0 87 set v(y) 0 88 set v(mode) column 89 set v(header) on 90 $w.t mark set insert end 91 $w.t tag config ok -foreground blue 92 $w.t tag config err -foreground red 93 $w.t insert end $v(prompt) 94 $w.t mark set out 1.0 95 after idle "focus $w.t" 96 } 97 98 bind Sqlitecon <1> {sqlitecon::Button1 %W %x %y} 99 bind Sqlitecon <B1-Motion> {sqlitecon::B1Motion %W %x %y} 100 bind Sqlitecon <B1-Leave> {sqlitecon::B1Leave %W %x %y} 101 bind Sqlitecon <B1-Enter> {sqlitecon::cancelMotor %W} 102 bind Sqlitecon <ButtonRelease-1> {sqlitecon::cancelMotor %W} 103 bind Sqlitecon <KeyPress> {sqlitecon::Insert %W %A} 104 bind Sqlitecon <Left> {sqlitecon::Left %W} 105 bind Sqlitecon <Control-b> {sqlitecon::Left %W} 106 bind Sqlitecon <Right> {sqlitecon::Right %W} 107 bind Sqlitecon <Control-f> {sqlitecon::Right %W} 108 bind Sqlitecon <BackSpace> {sqlitecon::Backspace %W} 109 bind Sqlitecon <Control-h> {sqlitecon::Backspace %W} 110 bind Sqlitecon <Delete> {sqlitecon::Delete %W} 111 bind Sqlitecon <Control-d> {sqlitecon::Delete %W} 112 bind Sqlitecon <Home> {sqlitecon::Home %W} 113 bind Sqlitecon <Control-a> {sqlitecon::Home %W} 114 bind Sqlitecon <End> {sqlitecon::End %W} 115 bind Sqlitecon <Control-e> {sqlitecon::End %W} 116 bind Sqlitecon <Return> {sqlitecon::Enter %W} 117 bind Sqlitecon <KP_Enter> {sqlitecon::Enter %W} 118 bind Sqlitecon <Up> {sqlitecon::Prior %W} 119 bind Sqlitecon <Control-p> {sqlitecon::Prior %W} 120 bind Sqlitecon <Down> {sqlitecon::Next %W} 121 bind Sqlitecon <Control-n> {sqlitecon::Next %W} 122 bind Sqlitecon <Control-k> {sqlitecon::EraseEOL %W} 123 bind Sqlitecon <<Cut>> {sqlitecon::Cut %W} 124 bind Sqlitecon <<Copy>> {sqlitecon::Copy %W} 125 bind Sqlitecon <<Paste>> {sqlitecon::Paste %W} 126 bind Sqlitecon <<Clear>> {sqlitecon::Clear %W} 127 128 # Insert a single character at the insertion cursor 129 # 130 proc sqlitecon::Insert {w a} { 131 $w insert insert $a 132 $w yview insert 133 } 134 135 # Move the cursor one character to the left 136 # 137 proc sqlitecon::Left {w} { 138 upvar #0 $w v 139 scan [$w index insert] %d.%d row col 140 if {$col>$v(plength)} { 141 $w mark set insert "insert -1c" 142 } 143 } 144 145 # Erase the character to the left of the cursor 146 # 147 proc sqlitecon::Backspace {w} { 148 upvar #0 $w v 149 scan [$w index insert] %d.%d row col 150 if {$col>$v(plength)} { 151 $w delete {insert -1c} 152 } 153 } 154 155 # Erase to the end of the line 156 # 157 proc sqlitecon::EraseEOL {w} { 158 upvar #0 $w v 159 scan [$w index insert] %d.%d row col 160 if {$col>=$v(plength)} { 161 $w delete insert {insert lineend} 162 } 163 } 164 165 # Move the cursor one character to the right 166 # 167 proc sqlitecon::Right {w} { 168 $w mark set insert "insert +1c" 169 } 170 171 # Erase the character to the right of the cursor 172 # 173 proc sqlitecon::Delete w { 174 $w delete insert 175 } 176 177 # Move the cursor to the beginning of the current line 178 # 179 proc sqlitecon::Home w { 180 upvar #0 $w v 181 scan [$w index insert] %d.%d row col 182 $w mark set insert $row.$v(plength) 183 } 184 185 # Move the cursor to the end of the current line 186 # 187 proc sqlitecon::End w { 188 $w mark set insert {insert lineend} 189 } 190 191 # Add a line to the history 192 # 193 proc sqlitecon::addHistory {w line} { 194 upvar #0 $w v 195 if {$v(historycnt)>0} { 196 set last [lindex $v(history) [expr $v(historycnt)-1]] 197 if {[string compare $last $line]} { 198 lappend v(history) $line 199 incr v(historycnt) 200 } 201 } else { 202 set v(history) [list $line] 203 set v(historycnt) 1 204 } 205 set v(current) $v(historycnt) 206 } 207 208 # Called when "Enter" is pressed. Do something with the line 209 # of text that was entered. 210 # 211 proc sqlitecon::Enter w { 212 upvar #0 $w v 213 scan [$w index insert] %d.%d row col 214 set start $row.$v(plength) 215 set line [$w get $start "$start lineend"] 216 $w insert end \n 217 $w mark set out end 218 if {$v(prior)==""} { 219 set cmd $line 220 } else { 221 set cmd $v(prior)\n$line 222 } 223 if {[string index $cmd 0]=="." || [$v(db) complete $cmd]} { 224 regsub -all {\n} [string trim $cmd] { } cmd2 225 addHistory $w $cmd2 226 set rc [catch {DoCommand $w $cmd} res] 227 if {![winfo exists $w]} return 228 if {$rc} { 229 $w insert end $res\n err 230 } elseif {[string length $res]>0} { 231 $w insert end $res\n ok 232 } 233 set v(prior) {} 234 $w insert end $v(prompt) 235 } else { 236 set v(prior) $cmd 237 regsub -all {[^ ]} $v(prompt) . x 238 $w insert end $x 239 } 240 $w mark set insert end 241 $w mark set out {insert linestart} 242 $w yview insert 243 } 244 245 # Execute a single SQL command. Pay special attention to control 246 # directives that begin with "." 247 # 248 # The return value is the text output from the command, properly 249 # formatted. 250 # 251 proc sqlitecon::DoCommand {w cmd} { 252 upvar #0 $w v 253 set mode $v(mode) 254 set header $v(header) 255 if {[regexp {^(\.[a-z]+)} $cmd all word]} { 256 if {$word==".mode"} { 257 regexp {^.[a-z]+ +([a-z]+)} $cmd all v(mode) 258 return {} 259 } elseif {$word==".exit"} { 260 destroy [winfo toplevel $w] 261 return {} 262 } elseif {$word==".header"} { 263 regexp {^.[a-z]+ +([a-z]+)} $cmd all v(header) 264 return {} 265 } elseif {$word==".tables"} { 266 set mode multicolumn 267 set cmd {SELECT name FROM sqlite_master WHERE type='table' 268 UNION ALL 269 SELECT name FROM sqlite_temp_master WHERE type='table'} 270 $v(db) eval {PRAGMA database_list} { 271 if {$name!="temp" && $name!="main"} { 272 append cmd "UNION ALL SELECT name FROM $name.sqlite_master\ 273 WHERE type='table'" 274 } 275 } 276 append cmd { ORDER BY 1} 277 } elseif {$word==".fullschema"} { 278 set pattern % 279 regexp {^.[a-z]+ +([^ ]+)} $cmd all pattern 280 set mode list 281 set header 0 282 set cmd "SELECT sql FROM sqlite_master WHERE tbl_name LIKE '$pattern' 283 AND sql NOT NULL UNION ALL SELECT sql FROM sqlite_temp_master 284 WHERE tbl_name LIKE '$pattern' AND sql NOT NULL" 285 $v(db) eval {PRAGMA database_list} { 286 if {$name!="temp" && $name!="main"} { 287 append cmd " UNION ALL SELECT sql FROM $name.sqlite_master\ 288 WHERE tbl_name LIKE '$pattern' AND sql NOT NULL" 289 } 290 } 291 } elseif {$word==".schema"} { 292 set pattern % 293 regexp {^.[a-z]+ +([^ ]+)} $cmd all pattern 294 set mode list 295 set header 0 296 set cmd "SELECT sql FROM sqlite_master WHERE name LIKE '$pattern' 297 AND sql NOT NULL UNION ALL SELECT sql FROM sqlite_temp_master 298 WHERE name LIKE '$pattern' AND sql NOT NULL" 299 $v(db) eval {PRAGMA database_list} { 300 if {$name!="temp" && $name!="main"} { 301 append cmd " UNION ALL SELECT sql FROM $name.sqlite_master\ 302 WHERE name LIKE '$pattern' AND sql NOT NULL" 303 } 304 } 305 } else { 306 return \ 307 ".exit\n.mode line|list|column\n.schema ?TABLENAME?\n.tables" 308 } 309 } 310 set res {} 311 if {$mode=="list"} { 312 $v(db) eval $cmd x { 313 set sep {} 314 foreach col $x(*) { 315 append res $sep$x($col) 316 set sep | 317 } 318 append res \n 319 } 320 if {[info exists x(*)] && $header} { 321 set sep {} 322 set hdr {} 323 foreach col $x(*) { 324 append hdr $sep$col 325 set sep | 326 } 327 set res $hdr\n$res 328 } 329 } elseif {[string range $mode 0 2]=="col"} { 330 set y {} 331 $v(db) eval $cmd x { 332 foreach col $x(*) { 333 if {![info exists cw($col)] || $cw($col)<[string length $x($col)]} { 334 set cw($col) [string length $x($col)] 335 } 336 lappend y $x($col) 337 } 338 } 339 if {[info exists x(*)] && $header} { 340 set hdr {} 341 set ln {} 342 set dash --------------------------------------------------------------- 343 append dash ------------------------------------------------------------ 344 foreach col $x(*) { 345 if {![info exists cw($col)] || $cw($col)<[string length $col]} { 346 set cw($col) [string length $col] 347 } 348 lappend hdr $col 349 lappend ln [string range $dash 1 $cw($col)] 350 } 351 set y [concat $hdr $ln $y] 352 } 353 if {[info exists x(*)]} { 354 set format {} 355 set arglist {} 356 set arglist2 {} 357 set i 0 358 foreach col $x(*) { 359 lappend arglist x$i 360 append arglist2 " \$x$i" 361 incr i 362 append format " %-$cw($col)s" 363 } 364 set format [string trimleft $format]\n 365 if {[llength $arglist]>0} { 366 foreach $arglist $y "append res \[format [list $format] $arglist2\]" 367 } 368 } 369 } elseif {$mode=="multicolumn"} { 370 set y [$v(db) eval $cmd] 371 set max 0 372 foreach e $y { 373 if {$max<[string length $e]} {set max [string length $e]} 374 } 375 set ncol [expr {int(80/($max+2))}] 376 if {$ncol<1} {set ncol 1} 377 set nelem [llength $y] 378 set nrow [expr {($nelem+$ncol-1)/$ncol}] 379 set format "%-${max}s" 380 for {set i 0} {$i<$nrow} {incr i} { 381 set j $i 382 while 1 { 383 append res [format $format [lindex $y $j]] 384 incr j $nrow 385 if {$j>=$nelem} break 386 append res { } 387 } 388 append res \n 389 } 390 } else { 391 $v(db) eval $cmd x { 392 foreach col $x(*) {append res "$col = $x($col)\n"} 393 append res \n 394 } 395 } 396 return [string trimright $res] 397 } 398 399 # Change the line to the previous line 400 # 401 proc sqlitecon::Prior w { 402 upvar #0 $w v 403 if {$v(current)<=0} return 404 incr v(current) -1 405 set line [lindex $v(history) $v(current)] 406 sqlitecon::SetLine $w $line 407 } 408 409 # Change the line to the next line 410 # 411 proc sqlitecon::Next w { 412 upvar #0 $w v 413 if {$v(current)>=$v(historycnt)} return 414 incr v(current) 1 415 set line [lindex $v(history) $v(current)] 416 sqlitecon::SetLine $w $line 417 } 418 419 # Change the contents of the entry line 420 # 421 proc sqlitecon::SetLine {w line} { 422 upvar #0 $w v 423 scan [$w index insert] %d.%d row col 424 set start $row.$v(plength) 425 $w delete $start end 426 $w insert end $line 427 $w mark set insert end 428 $w yview insert 429 } 430 431 # Called when the mouse button is pressed at position $x,$y on 432 # the console widget. 433 # 434 proc sqlitecon::Button1 {w x y} { 435 global tkPriv 436 upvar #0 $w v 437 set v(mouseMoved) 0 438 set v(pressX) $x 439 set p [sqlitecon::nearestBoundry $w $x $y] 440 scan [$w index insert] %d.%d ix iy 441 scan $p %d.%d px py 442 if {$px==$ix} { 443 $w mark set insert $p 444 } 445 $w mark set anchor $p 446 focus $w 447 } 448 449 # Find the boundry between characters that is nearest 450 # to $x,$y 451 # 452 proc sqlitecon::nearestBoundry {w x y} { 453 set p [$w index @$x,$y] 454 set bb [$w bbox $p] 455 if {![string compare $bb ""]} {return $p} 456 if {($x-[lindex $bb 0])<([lindex $bb 2]/2)} {return $p} 457 $w index "$p + 1 char" 458 } 459 460 # This routine extends the selection to the point specified by $x,$y 461 # 462 proc sqlitecon::SelectTo {w x y} { 463 upvar #0 $w v 464 set cur [sqlitecon::nearestBoundry $w $x $y] 465 if {[catch {$w index anchor}]} { 466 $w mark set anchor $cur 467 } 468 set anchor [$w index anchor] 469 if {[$w compare $cur != $anchor] || (abs($v(pressX) - $x) >= 3)} { 470 if {$v(mouseMoved)==0} { 471 $w tag remove sel 0.0 end 472 } 473 set v(mouseMoved) 1 474 } 475 if {[$w compare $cur < anchor]} { 476 set first $cur 477 set last anchor 478 } else { 479 set first anchor 480 set last $cur 481 } 482 if {$v(mouseMoved)} { 483 $w tag remove sel 0.0 $first 484 $w tag add sel $first $last 485 $w tag remove sel $last end 486 update idletasks 487 } 488 } 489 490 # Called whenever the mouse moves while button-1 is held down. 491 # 492 proc sqlitecon::B1Motion {w x y} { 493 upvar #0 $w v 494 set v(y) $y 495 set v(x) $x 496 sqlitecon::SelectTo $w $x $y 497 } 498 499 # Called whenever the mouse leaves the boundries of the widget 500 # while button 1 is held down. 501 # 502 proc sqlitecon::B1Leave {w x y} { 503 upvar #0 $w v 504 set v(y) $y 505 set v(x) $x 506 sqlitecon::motor $w 507 } 508 509 # This routine is called to automatically scroll the window when 510 # the mouse drags offscreen. 511 # 512 proc sqlitecon::motor w { 513 upvar #0 $w v 514 if {![winfo exists $w]} return 515 if {$v(y)>=[winfo height $w]} { 516 $w yview scroll 1 units 517 } elseif {$v(y)<0} { 518 $w yview scroll -1 units 519 } else { 520 return 521 } 522 sqlitecon::SelectTo $w $v(x) $v(y) 523 set v(timer) [after 50 sqlitecon::motor $w] 524 } 525 526 # This routine cancels the scrolling motor if it is active 527 # 528 proc sqlitecon::cancelMotor w { 529 upvar #0 $w v 530 catch {after cancel $v(timer)} 531 catch {unset v(timer)} 532 } 533 534 # Do a Copy operation on the stuff currently selected. 535 # 536 proc sqlitecon::Copy w { 537 if {![catch {set text [$w get sel.first sel.last]}]} { 538 clipboard clear -displayof $w 539 clipboard append -displayof $w $text 540 } 541 } 542 543 # Return 1 if the selection exists and is contained 544 # entirely on the input line. Return 2 if the selection 545 # exists but is not entirely on the input line. Return 0 546 # if the selection does not exist. 547 # 548 proc sqlitecon::canCut w { 549 set r [catch { 550 scan [$w index sel.first] %d.%d s1x s1y 551 scan [$w index sel.last] %d.%d s2x s2y 552 scan [$w index insert] %d.%d ix iy 553 }] 554 if {$r==1} {return 0} 555 if {$s1x==$ix && $s2x==$ix} {return 1} 556 return 2 557 } 558 559 # Do a Cut operation if possible. Cuts are only allowed 560 # if the current selection is entirely contained on the 561 # current input line. 562 # 563 proc sqlitecon::Cut w { 564 if {[sqlitecon::canCut $w]==1} { 565 sqlitecon::Copy $w 566 $w delete sel.first sel.last 567 } 568 } 569 570 # Do a paste opeation. 571 # 572 proc sqlitecon::Paste w { 573 if {[sqlitecon::canCut $w]==1} { 574 $w delete sel.first sel.last 575 } 576 if {[catch {selection get -displayof $w -selection CLIPBOARD} topaste] 577 && [catch {selection get -displayof $w -selection PRIMARY} topaste]} { 578 return 579 } 580 if {[info exists ::$w]} { 581 set prior 0 582 foreach line [split $topaste \n] { 583 if {$prior} { 584 sqlitecon::Enter $w 585 update 586 } 587 set prior 1 588 $w insert insert $line 589 } 590 } else { 591 $w insert insert $topaste 592 } 593 } 594 595 # Enable or disable entries in the Edit menu 596 # 597 proc sqlitecon::EnableEditMenu w { 598 upvar #0 $w.t v 599 set m $v(editmenu) 600 if {$m=="" || ![winfo exists $m]} return 601 switch [sqlitecon::canCut $w.t] { 602 0 { 603 $m entryconf Copy -state disabled 604 $m entryconf Cut -state disabled 605 } 606 1 { 607 $m entryconf Copy -state normal 608 $m entryconf Cut -state normal 609 } 610 2 { 611 $m entryconf Copy -state normal 612 $m entryconf Cut -state disabled 613 } 614 } 615 } 616 617 # Prompt the user for the name of a writable file. Then write the 618 # entire contents of the console screen to that file. 619 # 620 proc sqlitecon::SaveFile w { 621 set types { 622 {{Text Files} {.txt}} 623 {{All Files} *} 624 } 625 set f [tk_getSaveFile -filetypes $types -title "Write Screen To..."] 626 if {$f!=""} { 627 if {[catch {open $f w} fd]} { 628 tk_messageBox -type ok -icon error -message $fd 629 } else { 630 puts $fd [string trimright [$w get 1.0 end] \n] 631 close $fd 632 } 633 } 634 } 635 636 # Erase everything from the console above the insertion line. 637 # 638 proc sqlitecon::Clear w { 639 $w delete 1.0 {insert linestart} 640 } 641 642 # An in-line editor for SQL 643 # 644 proc sqlitecon::_edit {origtxt {title {}}} { 645 for {set i 0} {[winfo exists .ed$i]} {incr i} continue 646 set w .ed$i 647 toplevel $w 648 wm protocol $w WM_DELETE_WINDOW "$w.b.can invoke" 649 wm title $w {Inline SQL Editor} 650 frame $w.b 651 pack $w.b -side bottom -fill x 652 button $w.b.can -text Cancel -width 6 -command [list set ::$w 0] 653 button $w.b.ok -text OK -width 6 -command [list set ::$w 1] 654 button $w.b.cut -text Cut -width 6 -command [list ::sqlitecon::Cut $w.t] 655 button $w.b.copy -text Copy -width 6 -command [list ::sqlitecon::Copy $w.t] 656 button $w.b.paste -text Paste -width 6 -command [list ::sqlitecon::Paste $w.t] 657 set ::$w {} 658 pack $w.b.cut $w.b.copy $w.b.paste $w.b.can $w.b.ok\ 659 -side left -padx 5 -pady 5 -expand 1 660 if {$title!=""} { 661 label $w.title -text $title 662 pack $w.title -side top -padx 5 -pady 5 663 } 664 text $w.t -bg white -fg black -yscrollcommand [list $w.sb set] 665 pack $w.t -side left -fill both -expand 1 666 scrollbar $w.sb -orient vertical -command [list $w.t yview] 667 pack $w.sb -side left -fill y 668 $w.t insert end $origtxt 669 670 vwait ::$w 671 672 if {[set ::$w]} { 673 set txt [string trimright [$w.t get 1.0 end]] 674 } else { 675 set txt $origtxt 676 } 677 destroy $w 678 return $txt 679 } 680