-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathconnect.tcl
435 lines (349 loc) · 12 KB
/
connect.tcl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
#! /usr/bin/env tclsh
package require Tcl 8.5 ;# dict, {*}
package require Tk 8.5 ;# ttk
package require tdbc
package require cargocult::tk
package require cargocult::widgets
package require snit 2.2
namespace eval dbluejay {
# Knob widgets: allow the user to twiddle the various divers knobs that pertain
# to TDBC database connections. Except as noted, the megawidgets in this
# namespace correspond to TDBC drivers of the same names. Each widget is
# expected to support the connect method, which should return a dict of the form
# expected by dbluejay::metabrowser's add_connection method, hopefully
# describing a brand-new TDBC database connection that matches the user's
# specifications.
namespace eval knobs {
snit::widgetadaptor mysql {
delegate method * to hull
delegate option * to hull
constructor {args} {
installhull using ::cargocult::optionlist -rows {
host Host: ttk::entry {} 0 localhost
port Port: ttk::spinbox {
-from 0 -to 65536 -increment 1
} 0 {3306}
socket Socket: ttk::entry {} 0 {}
user Username: ttk::entry {} 0 {}
password Password: ttk::entry {-show *} 0 {}
database Database: ttk::entry {} 0 {}
ssl_ca {TLS CA:} ttk::entry {} 0 {}
ssl_capath {TLS CA search path:} ttk::entry {} 0 {}
ssl_cert {TLS certificate:} ttk::entry {} 0 {}
ssl_cipher {TLS cipher:} ttk::entry {} 0 {}
ssl_key {TLS key:} ttk::entry {} 0 {}
}
$self configurelist $args
}
method connect {} {
tdbc::mysql::connection new {*}[$self option_list]
}
method personality {} {
return information_schema
}
}
snit::widget odbc {
hulltype ttk::frame
component optrows ;# cargocult::dynrows
delegate method * to hull
delegate option * to hull
variable mode ;# Driver/DSN
variable Driver {}
variable DSN {}
variable connstr {}
variable personality {none}
constructor {args} {
ttk::radiobutton $win.drivercheck -text Driver -variable [
myvar mode
] -value Driver -command [mymethod Gen_connstr]
ttk::combobox $win.drivers -values [
dict keys [tdbc::odbc::drivers]
] -textvariable [myvar Driver] -validatecommand [
mymethod Gen_connstr
] -validate focus
ttk::radiobutton $win.dsncheck -text {Data Source} -variable [
myvar mode
] -value DSN -command [mymethod Gen_connstr]
ttk::combobox $win.dsns -values [
dict keys [tdbc::odbc::datasources]
] -textvariable [myvar DSN] -validatecommand [
mymethod Gen_connstr
] -validate focus
ttk::label $win.personalityl -text {SQL personality:}
ttk::combobox $win.personality -textvariable [
myvar personality
] -state readonly -values [
::dbluejay::personality::generic_personalities
]
install optrows using cargocult::dynrows $win.optrows \
-newrow cargocult::kvpair -rowopts [list \
-key_validatecommand [mymethod Gen_connstr] \
-key_validate focus \
-value_validatecommand [mymethod Gen_connstr] \
-value_validate focus
]
foreach {tag event} [list \
$win <<ComboboxSelected>> \
$optrows <<NewDynrow>> \
$optrows <<RmDynrow>>
] {
bind $tag $event [list after idle [mymethod Gen_connstr]]
}
set mode Driver
set Driver [lindex [dict keys [tdbc::odbc::drivers]] 0]
set DSN [lindex [dict keys [tdbc::odbc::datasources]] 0]
$self Gen_connstr
ttk::entry $win.connstren -textvariable [myvar connstr]
grid $win.drivercheck $win.drivers $win.dsncheck $win.dsns -sticky new
grid $win.personalityl $win.personality x x -sticky ew
grid $optrows - - - -sticky nsew
grid $win.connstren - - - -sticky sew
grid rowconfigure $win 2 -weight 1
grid columnconfigure $win {1 3} -weight 1
cargocult::pad_grid_widgets [winfo children $win]
$self configurelist $args
}
# Translate the various knobs into an ODBC connection string.
method Gen_connstr {} {
dict set conndict $mode [set $mode]
foreach row [$optrows rows] {
dict set conndict [$row cget -key] [$row cget -value]
}
set connstr \;[join [lmap {key value} $conndict {
lindex $key=$value
}] \;]
return true
}
method connect {} {
$self Gen_connstr
tdbc::odbc::connection new $connstr
}
method personality {} {
return $personality
}
}
snit::widgetadaptor postgres {
delegate method * to hull
delegate option * to hull
constructor {args} {
installhull using ::cargocult::optionlist -rows {
host Hostname: ttk::entry {} 0 localhost
hostaddr {IP address:} ttk::entry {} 0 127.0.0.1
port Port: ttk::spinbox {
-from 0 -to 65536 -increment 1
} 0 {3306}
user Username: ttk::entry {} 0 {}
password Password: ttk::entry {-show *} 0 {}
database Database: ttk::entry {} 0 {}
options {Additional options:} ttk::entry {} 0 {}
sslmode {SSL mode:} ttk::combobox {-values {
disable
allow
prefer
require
}} 0 prefer
service {Service name:} ttk::entry {} 0 {}
tty {Debug TTY (obsolete):} ttk::entry {} 0 {}
}
$self configurelist $args
}
method connect {} {
tdbc::postgres::connection new {*}[$self option_list]
}
method personality {} {
# The plain information_schema personality will work,
# but causes serious performance problems when postgres
# happily includes every single built-in function in
# information_schema.routines
return postgres
}
}
# Meant principally for *existing* databases, though it won't validate
# that. (tk_getOpenFile refuses to create nonexistent files.)
snit::widget sqlite3 {
hulltype ttk::frame
delegate method * to hull
delegate option * to hull
variable filename
constructor {args} {
ttk::label $win.filenamel -text "File:"
ttk::entry $win.filenameen -textvariable [myvar filename]
ttk::button $win.browse -text "Browse..." -command [
mymethod Browse
]
grid $win.filenamel $win.filenameen -sticky new
grid x $win.browse -sticky se
grid rowconfigure $win {0 1} -weight 1
grid columnconfigure $win 1 -weight 1
::cargocult::pad_grid_widgets [winfo children $win]
$self configurelist $args
}
method connect {} {
tdbc::sqlite3::connection new $filename
}
method Browse {} {
set filename [tk_getOpenFile -filetypes {
{{SQLite databases} {.sqlite} BINA}
{{SQLite databases} {.db} BINA}
{{All files} *}
} -parent $win -title "Select existing SQLite database file"]
}
method personality {} {
return sqlite3
}
}
# Generic widget allowing the user to specify a custom TDBC driver, by
# specifying the name of a new package to load and a Tcl command to
# evaluate to get a TDBC database handle. This is, by anybody's
# standard, a very direct code-execution vector, and should presumably
# be treated accordingly.
snit::widget other {
hulltype ttk::frame
delegate method * to hull
delegate option * to hull
variable package
variable conncmd
variable personality none
constructor {args} {
ttk::label $win.packagel -text "Driver package name:"
ttk::entry $win.packageen -textvariable [myvar package]
ttk::label $win.conncmdl -text "Connection command:"
ttk::entry $win.conncmden -textvariable [myvar conncmd]
ttk::label $win.personalityl -text {SQL personality:}
ttk::combobox $win.personality -textvariable [
myvar personality
] -state readonly -values [::dbluejay::personality::personalities]
grid $win.packagel $win.packageen -sticky new
grid $win.conncmdl $win.conncmden -sticky new
grid $win.personalityl $win.personality -sticky new
grid columnconfigure $win 1 -weight 1
::cargocult::pad_grid_widgets [winfo children $win]
$self configurelist $args
}
method connect {} {
package require $package
eval $conncmd
}
method personality {} {
return $personality
}
}
}
# Top-level window wrapping one of the knobs widgets above, also used to twiddle
# driver-independent knobs (currently knob, singular, the connection's human-
# readable "nickname"). Generates the synthetic event <<NewConnection>> upon
# the user's choosing to create a new database connection (at which time this
# window will destroy itself), with its -data field containing a dict with the
# following keys:
# db: the new database connection's TDBC handle
# nickname: User-visible nickname of this connection
# personality: Name of the database personality to use (see personality.tcl)
snit::widget connectdialog {
hulltype toplevel
component knobs ;# any member of dbluejay::knobs above
delegate method * to hull
delegate option * to hull
# The name of a specific member of dbluejay::knobs to wrap, which as
# explained above is either also the name of a TDBC driver or the word
# "other".
option -driver -readonly yes -default other
variable nickname {}
typevariable nickserial 0
constructor {args} {
$self configurelist $args
set nickname "Unnamed [incr nickserial] ([$self cget -driver])"
set f [ttk::frame $win.f]
install knobs using knobs::[$self cget -driver] $f.knobs
ttk::label $f.namel -text "Nickname:"
ttk::entry $f.nameen -textvariable [myvar nickname]
ttk::button $f.connect -text "Connect" -command [mymethod Connect]
grid $knobs - - -sticky nsew
grid $f.namel $f.nameen $f.connect -sticky sew
grid rowconfigure $f 0 -weight 1
grid columnconfigure $f 1 -weight 1
grid $f -sticky nsew
grid rowconfigure $win 0 -weight 1
grid columnconfigure $win 0 -weight 1
}
method Connect {} {
event generate [winfo parent $win] <<NewConnection>> -data [
dict create \
db [$knobs connect] \
nickname $nickname \
personality [$knobs personality]
]
destroy $win
}
}
variable KNOWN_DRIVERS {
mysql {MySQL}
odbc {ODBC}
postgres {PostgreSQL}
sqlite3 {Existing SQLite3 database}
}
variable loaded_drivers
# Load a TDBC driver by name (in KNOWN_DRIVERS above) and package name.
proc load_driver {name package} {
variable loaded_drivers
if {[catch [list package require $package] msg]} {
return -code error "couldn't load $name driver: $msg"
} else {
dict set loaded_drivers $name $msg
return $msg
}
}
# Iterate over KNOWN_DRIVERS above, loading whichever among them can be loaded.
proc driversearch {} {
variable KNOWN_DRIVERS
foreach driver [dict keys $KNOWN_DRIVERS] {
if {[catch [list load_driver $driver tdbc::$driver] msg]} {
puts stderr "warning: $msg"
}
}
}
# Creates a [menu] with path $path containing options corresponding to each
# currently-loadable TDBC driver (and "other"), each of which spawns a
# connectdialog wrapping the appropriate knobs:: widget. connectdialogs will be
# modal with respect to $rootwin.
proc connectmenu {path rootwin} {
variable KNOWN_DRIVERS
variable loaded_drivers
set menu [menu $path]
driversearch
dict for {driver version} $loaded_drivers {
$menu add command -command [namespace code [
list show_connectdialog $driver $rootwin
]] -label [dict get $KNOWN_DRIVERS $driver]...
if {$driver eq {sqlite3}} {
$menu add command -command [
namespace code [list new_sqlite $rootwin]
] -label {New SQLite3 database...}
}
}
$menu add command -command [namespace code [
list show_connectdialog other $rootwin
]] -label {Other TDBC driver...}
}
# Show a connectdialog widget wrapping the knobs:: member $driver, modal with
# respect to $rootwin.
proc show_connectdialog {driver rootwin} {
set dialog [
connectdialog $rootwin.[cargocult::gensym connectdialog] -driver $driver
]
cargocult::modalize $dialog $rootwin
}
# Open a tdbc::sqlite3::connection object on a new file chosen by the user, and
# generate a <<NewConnection>> event from $rootwin, with an appropriate dict in
# its -data field (as emitted by connectdialog).
proc new_sqlite {rootwin} {
if {[set dbfile [tk_getSaveFile -filetypes {
{{SQLite databases} {.sqlite} BINA}
{{SQLite databases} {.db} BINA}
{{All files} *}
} -parent $rootwin -title "Create new SQLite database file"]] ne {}} {
event generate $rootwin <<NewConnection>> -data [dict create db [
tdbc::sqlite3::connection new $dbfile
] nickname $dbfile personality sqlite3]
}
}
} ;# namespace eval dbluejay