blob: b5dbcafc2a5a2a11caef7002391fb66bda47fff8 [file] [log] [blame]
# 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
}