Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Newer
Older
100755 365 lines (314 sloc) 11.521 kb
618d43c @shish initial import
authored
1 #!/usr/bin/python
2
3 from __future__ import with_statement
4
5 import sqlite3
6 import curses
7 import sys
8 import signal
9 import os
10 import csui
11 import math
12 import re
13 from optparse import OptionParser
14 import logging
15 import tempfile
16
17 version = "0.1"
18
19
20 # these should be part of their libraries
21
22 class screen:
23 def __init__(self):
24 self.stdscr = None
25
26 def __enter__(self):
27 self.stdscr = curses.initscr()
28 self.stdscr.keypad(1)
29 curses.noecho()
30 return self.stdscr
31
32 def __exit__(self, type, value, traceback):
33 curses.nocbreak()
34 if self.stdscr:
35 self.stdscr.keypad(0)
36 curses.echo()
37 curses.endwin()
38
39 class dbcur:
40 def __init__(self, fname):
41 self.fname = fname
42
43 def __enter__(self):
44 self.conn = sqlite3.connect(self.fname)
45 self.c = self.conn.cursor()
46 return self.c
47
8f6e5b0 @shish much betterness
authored
48 def __exit__(self, extype, value, traceback):
618d43c @shish initial import
authored
49 self.c.close()
8f6e5b0 @shish much betterness
authored
50 if extype:
51 self.conn.rollback()
52 else:
53 self.conn.commit()
618d43c @shish initial import
authored
54 self.conn.close()
55
56 def do_outside_curses(func, *args, **kw):
57 """
58 Leave curses mode before running a function, then re-enter it afterwards.
59
60 Useful when eg you want to see the output of a command line app.
61 """
62 value = None
63 old_winch = signal.signal(signal.SIGWINCH, signal.SIG_IGN)
64 old_cursor = csui.curs_set(1)
65 curses.endwin()
66
67 try:
68 os.system("clear")
69 value = func(*args, **kw)
70 os.system("clear")
71 except Exception, e:
72 print "The thing outside curses had an error"
73 value = None
74
75 curses.initscr()
76 csui.curs_set(old_cursor)
77
78 # bug in python? old_winch = 0, so we have to define our own :-/
79 def __sigwinch_handler(signal, frame):
80 curses.endwin()
81 curses.initscr()
82 signal.signal(signal.SIGWINCH, __sigwinch_handler)
83 return value
84
85
86 # these should be part of some standard library
87
88 def limit(val, a, b):
89 n = min(a, b)
90 x = max(a, b)
91 if val < n: val = n
92 if val > x: val = x
93 return val
94
8f6e5b0 @shish much betterness
authored
95 def rowsafe(text, maxlen):
96 m = maxlen-1
97 safe = re.sub("[^\S ]", "", unicode(text).encode('ascii', 'ignore'))
98 if len(safe) < m:
99 lenned = safe
100 else:
101 lenned = safe[0:m-3]+"..."
102 return (" %-"+str(m)+"."+str(m)+"s") % lenned
618d43c @shish initial import
authored
103
104 # database abstraction
105
106 def get_tables(cur):
107 res = cur.execute("SELECT name FROM sqlite_master WHERE type='table'")
108 tables = list([row[0] for row in res])
109 return tables
110
111 # functions
112
8f6e5b0 @shish much betterness
authored
113 def make_limiter(names, values):
114 parts = []
115 args = []
116 for a, b in zip(names, values):
117 if b:
118 parts.append("%s=?" % a)
119 args.append(b)
120 else:
121 parts.append("%s IS NULL")
122 return (" AND ".join(parts), args)
123
618d43c @shish initial import
authored
124 def edit_external(value):
125 (fd, tmpname) = tempfile.mkstemp(prefix="csb")
126 os.close(fd)
127
128 if value:
129 fp = file(tmpname, "w")
130 fp.write(value)
131 fp.close()
132
133 editor = "vim"
134 if 'EDITOR' in os.environ:
135 editor = os.environ['EDITOR']
136 do_outside_curses(os.system, editor+" "+tmpname)
137
138 fp = file(tmpname, "r")
139 value = fp.read()
140 fp.close()
141
142 os.unlink(tmpname)
143
144 return value
145
146 def edit(stdscr, cur, table, col_names, col_values, col, external):
147 if external:
148 val = edit_external(col_values[col_names.index(col)])
149 else:
150 val = csui.get_string(stdscr, "New value for "+col+" column")
8f6e5b0 @shish much betterness
authored
151 (ands, largs) = make_limiter(col_names, col_values)
152 query = "UPDATE %s SET %s=? WHERE %s" % (table, col, ands)
618d43c @shish initial import
authored
153 args = [val, ]
8f6e5b0 @shish much betterness
authored
154 args.extend(largs)
618d43c @shish initial import
authored
155 cur.execute(query, args)
156 logging.info(query)
157
158 def main(args):
8f6e5b0 @shish much betterness
authored
159 parser = OptionParser(version=version)
618d43c @shish initial import
authored
160 #parser.add_option("-w", "--width", dest="width", default=20,
161 # help="set the default column width to COLS", metavar="COLS")
162 parser.add_option("-t", "--table", dest="table", default=None,
163 help="start with table NAME open", metavar="NAME")
164 parser.add_option("-d", "--debug",
165 action="store_true", dest="debug", default=False,
166 help="turn on debugging to csb.log")
6c332df @shish man page
authored
167 parser.add_option("-y", "--yes",
618d43c @shish initial import
authored
168 action="store_true", dest="yes", default=False,
169 help="Do things without confirming")
170 (options, args) = parser.parse_args()
171
172 if len(args) == 1:
8f6e5b0 @shish much betterness
authored
173 options.fname = args[0]
618d43c @shish initial import
authored
174 else:
6c332df @shish man page
authored
175 options.fname = raw_input("Enter database uri: ")
618d43c @shish initial import
authored
176
177 if options.debug:
178 logging.basicConfig(
179 level=logging.DEBUG,
180 format='%(asctime)s %(levelname)-8s %(message)s',
181 filename="csb.log"
182 )
183 else:
184 logging.basicConfig(
185 level=logging.CRITICAL,
186 format='%(asctime)s %(levelname)-8s %(message)s'
187 )
188
8f6e5b0 @shish much betterness
authored
189 try:
190 with dbcur(options.fname) as cur:
191 tables = get_tables(cur)
192 if len(tables) == 0:
193 print "No tables in database"
194 return 1
195 if not options.table or options.table not in tables:
196 options.table = get_tables(cur)[0]
197 with screen() as stdscr:
198 csui.curs_set(False)
199 curses.mousemask(0xFFFFFFF)
200 main_loop(cur, stdscr, options)
201 except KeyboardInterrupt:
202 print "Exited without committing changes"
203 return 2
618d43c @shish initial import
authored
204
205 return 0
206
8f6e5b0 @shish much betterness
authored
207 def main_loop(cur, stdscr, options):
208 table = options.table
209 selected_row = 0
210 selected_col = 0
211 page = 0
212 page_count = 0
213 col_names = []
214 last_query = None
215 grid = None
216 while True:
217 #=============================================================
218 # calculate useful things
219
220 (height, width) = stdscr.getmaxyx()
221 page_size = height-3
222
223 #=============================================================
224 # get data
225
226 query = "SELECT * FROM %s LIMIT %d,%d" % (table, page_size*page, page_size)
227 if query != last_query:
228 page_count = math.ceil(float(list(cur.execute("SELECT count(*) FROM %s" % table))[0][0]) / page_size)
229 res = list(cur.execute(query))
230 row_count = len(res)
231 col_names = [tup[0] for tup in cur.description]
232 last_query = query
233 col_width = (width-1)/len(col_names)
234
235 #=============================================================
236 # draw data
237
238 # magic
239 title = "%s (%s) -- Page %d/%d -- CSB %s" % (
240 options.fname, table, page+1, page_count, version)
241 csui.set_title(title)
242 stdscr.refresh() # this makes things work and I don't know why
243
244 # title
245 titlebar = curses.newwin(1, width, 0, 0)
246 titlebar.addstr(0, 0, "")
247 titlebar.clrtoeol()
248 titlebar.addstr(0, (width-len(title))/2, title)
249 titlebar.refresh()
250
251 # grid
252 grid = curses.newwin(height-1, width, 1, 0)
253 _w = str(col_width-1)
254 col_fmt = " %-"+_w+"."+_w+"s"
255 for colid, col in enumerate(col_names):
256 grid.addstr(0, colid*col_width, col_fmt % col, curses.A_UNDERLINE)
257 for rowid, row in enumerate(res):
258 for colid, col in enumerate(row):
259 if rowid == selected_row and colid == selected_col:
260 grid.addstr(rowid+1, colid*col_width, rowsafe(col, col_width), curses.A_REVERSE)
261 else:
262 grid.addstr(rowid+1, colid*col_width, rowsafe(col, col_width))
263 grid.refresh()
264
265 #=============================================================
266 # input
267
268 c = stdscr.getch()
269 # global
270 if c == ord('q'):
271 if options.yes or csui.confirm(stdscr, "Commit and exit?"):
272 break
273 elif c == ord('x'):
274 if options.yes or csui.confirm(stdscr, "Exit without saving?"):
275 raise KeyboardInterrupt
276 elif c == ord('t'):
277 tables = get_tables(cur)
278 table = tables[csui.choose_option(stdscr, "Pick a table", tables)]
279 # grid
280 elif c == curses.KEY_UP: selected_row = selected_row - 1
281 elif c == curses.KEY_DOWN: selected_row = selected_row + 1
282 elif c == curses.KEY_LEFT: selected_col = selected_col - 1
283 elif c == curses.KEY_RIGHT: selected_col = selected_col + 1
284 elif c == curses.KEY_MOUSE:
285 (mouseid, x, y, z, bstate) = curses.getmouse()
286 selected_col = x/col_width
287 selected_row = y-2
288 # page
289 elif c == curses.KEY_PPAGE or c == ord('k'): page = page - 1
290 elif c == curses.KEY_NPAGE or c == ord('j'): page = page + 1
291 # display
292 #elif c == ord('+'): col_width = col_width + 1
293 #elif c == ord('-'): col_width = col_width - 1
294 # editing
295 elif c == ord('e'):
296 edit(
297 stdscr, cur, table, col_names,
298 res[selected_row],
299 col_names[selected_col], False)
300 last_query = None
301 elif c == ord('E'):
302 edit(
303 stdscr, cur, table, col_names,
304 res[selected_row],
305 col_names[selected_col], True)
306 last_query = None
307 elif c == curses.KEY_IC or c == ord('i'):
308 vals = []
309 qs = []
310 for col in col_names:
311 vals.append(csui.get_string(stdscr, col))
312 qs.append("?")
313 query = "INSERT INTO %s(%s) VALUES (%s)" % (table, ", ".join(col_names), ", ".join(qs))
314 if options.yes or csui.confirm(stdscr, [query, str(vals)]):
315 cur.execute(query, vals)
316 last_query = None
317 elif c == curses.KEY_DC or c == ord('d'):
318 (ands, args) = make_limiter(col_names, res[selected_row])
319 query = "DELETE FROM %s WHERE %s" % (table, ands)
320 if options.yes or csui.confirm(stdscr, [query, str(args)]):
321 cur.execute(query, args)
322 last_query = None
323 elif c == curses.KEY_ENTER or c == ord('v'):
324 csui.alert(stdscr, "Viewing Row",
325 [str(a)+": "+unicode(b) for a, b in zip(col_names, res[selected_row])]
326 )
327 elif c == ord('h') or c == ord('?'):
328 csui.alert(stdscr, "Keys", [
329 "t - select table",
330 "arrows - move over grid",
331 "mouse1 - select cell",
332 "pgup/dn - switch pages",
333 #"+ / - - resize columns",
334 "e - edit cell",
335 "E - edit in external editor",
336 "d - delete row",
337 "v - view row",
338 "h / ? - help",
339 "q - commit and quit",
340 "x - quit without committing",
341 ])
342
343 #=============================================================
344 # side effects of input
345
346 if selected_row < 0:
347 if page == 0:
348 selected_row = 0
349 else:
350 page = page - 1
351 selected_row = row_count-1
352 if selected_row > row_count - 1:
353 if page == page_count - 1:
354 selected_row = row_count-1
355 else:
356 page = page + 1
357 selected_row = 0
358 selected_col = limit(selected_col, 0, len(col_names)-1)
359 page = limit(page, 0, page_count-1)
360 col_width = limit(col_width, 5, 100)
361
618d43c @shish initial import
authored
362 if __name__ == "__main__":
363 sys.exit(main(sys.argv))
364
Something went wrong with that request. Please try again.