2 # -*- coding: utf-8 -*-
4 # Copyright 2009 Felix Rabe <public@felixrabe.net>
5 # Licensed under the terms of the GNU General Public License, version 3 or later.
16 from PyGTKShell
.API
import *
20 t
= datetime
.datetime
.now()
21 s
= t
.strftime("%Y-%m-%dT%H:%M:%S.%f")
22 return " #time#%s " % s
24 def get_automatic_tags(tags
):
25 res
= collections
.defaultdict(list)
26 for tag
in tags
.split():
27 if tag
.startswith("#"):
29 res
[tag
[1:i
]].append(tag
[i
+1:])
31 time
= datetime
.datetime
.strptime(res
["time"][0], "%Y-%m-%dT%H:%M:%S.%f")
40 def __init__(self
, filename
):
41 super(JotDB
, self
).__init
__()
42 self
.conn
= sqlite3
.connect(filename
)
43 self
.filename
= filename
44 self
.conn
.execute("""create table if not exists jotdb
45 (id integer primary key, note text, tags text)""")
46 self
.conn
.execute("""create table if not exists config
47 (key text primary key, value text)""")
50 def enter(self
, row_id
, note
, tags
): # row_id may be None to add a new row
51 # print "enter() %r %r %r" % (row_id, note, tags)
52 cur
= self
.conn
.cursor()
53 tags
= " %s " % " ".join(tags
.split())
56 if i
!= -1 and row_id
is not None:
57 # print "* removing %u" % row_id
58 cur
.execute("delete from jotdb where id = ?", (row_id
,))
61 row_id
= int(tags
[i
+len(s
):].split(None, 1)[0])
62 row_id
, note
, tags
= self
.get_item(row_id
)
63 if tags_note
: # erase all tags by entering " " (a space)
66 if not note
: # if no note is entered, erase the specified row
69 # print "* removing %u" % row_id
70 cur
.execute("delete from jotdb where id = ?", (row_id
,))
73 # print "* insert/replace %r %r %r" % (row_id, note, tags)
74 cur
.execute("insert or replace into jotdb values (?, ?, ?)",
76 last_id
= cur
.lastrowid
81 cur
= self
.conn
.cursor()
82 cur
.execute("select count(*) from jotdb")
83 return cur
.fetchone()[0]
85 def get_list(self
, tag_filter
=None): # TODO: implement tag filtering
86 cur
= self
.conn
.cursor()
87 cur
.execute("select * from jotdb")
90 def get_item(self
, row_id
):
91 cur
= self
.conn
.cursor()
93 cur
.execute("select * from jotdb order by id desc limit 1")
95 cur
.execute("select * from jotdb where id = ?", (row_id
,))
98 def get_config(self
, key
):
99 cur
= self
.conn
.cursor()
100 cur
.execute("select value from config where key = ?", (key
,))
101 value
= cur
.fetchone()
102 if value
is None: return None
105 def set_config(self
, key
, value
):
106 cur
= self
.conn
.cursor()
107 cur
.execute("insert or replace into config values (?, ?)", (key
, value
))
111 ## COMMAND EXECUTION LAYER
113 class CommandExec(object):
117 def __init__(self
, jotdb
):
118 super(CommandExec
, self
).__init
__()
121 def enter(self
, row_id
, note
, tags
):
122 if not note
.startswith(","): # not a command
123 self
._enter
(row_id
, note
, tags
)
125 command
= shlex
.split(note
[1:])
127 cmd
, args
= command
[0], command
[1:]
128 cmd_attr
= "_cmd_%s" % cmd
.lower()
129 if not hasattr(self
, cmd_attr
):
130 raise Exception, "Unknown command: %s" % cmd
131 getattr(self
, cmd_attr
)(row_id
, tags
, *args
)
133 self
._cb
_raise
_exception
(e
)
135 def _enter(self
, row_id
, note
, tags
, show_row
=False):
136 row_id
= self
.jotdb
.enter(row_id
, note
, tags
)
137 self
._cb
_refresh
_view
()
138 if show_row
and row_id
is not None:
139 self
._cb
_show
_row
(row_id
, show_row
)
142 # Callbacks (ordered alphabetically)
147 def _cb_raise_exception(self
, e
):
148 note
= "## ERROR: %s" % e
149 tags
= automatic_tags()
150 self
._enter
(None, note
, tags
, True)
152 def _cb_refresh_view(self
):
155 def _cb_show_row(self
, row_id
, how
):
158 # Commands (ordered alphabetically, long names first)
160 def _cmd_append(self
, row_id
, tags
):
164 row_id
, note
, tags
= self
.jotdb
.get_item(row_id
)
165 self
._enter
(row_id
, note
, tags
, "append")
168 def _cmd_delete(self
, row_id
, tags
):
172 row_id
, note
, tags
= self
.jotdb
.get_item(row_id
)
173 self
._enter
(row_id
, "", tags
)
176 def _cmd_edit(self
, row_id
, tags
):
180 row_id
, note
, tags
= self
.jotdb
.get_item(row_id
)
181 self
._enter
(row_id
, note
, tags
, True)
184 def _cmd_filter(self
, row_id
, tags
, cmd
, *args
):
186 List and edit filters.
188 Type ',filter list' to list all registered filters.
189 Type ',filter edit <name>' to edit or create a filter called <name>.
191 if "list".startswith(cmd
.lower()):
193 for row_id
, note
, tags
in self
.jotdb
.get_list():
194 atags
= get_automatic_tags(tags
)
195 filters
[atags
.get("filter")] = None
196 note
= ["List of filters:", ""]
198 if not name
: continue
199 note
= "\n".join(note
) + "\n"
200 tags
= automatic_tags()
201 self
._enter
(None, note
, tags
, True)
203 if "edit".startswith(cmd
.lower()):
205 for row_id
, note
, tags
in self
.jotdb
.get_list():
206 atags
= get_automatic_tags(tags
)
207 if name
in atags
["filter"]: break
210 note
= "# Filter: %s\n\n" % name
211 tags
= automatic_tags() + " #filter#" + name
212 row_id
= self
._enter
(row_id
, note
, tags
)
213 self
._cb
_show
_row
(row_id
, "append")
217 def _cmd_help(self
, row_id
, tags
, command
=None):
219 Get help about Jot or about a Jot command.
221 Type ',help' (comma + help) to get help.
222 Type ',help <command>' to get more help about a specific command.
224 general_note
= textwrap
.dedent("""
227 With Jot, you can easily jot down notes of all sorts. To manage
228 your notes, you can tag, filter, and sort them. You can even format
229 the metadata information that is displayed to the left of the note
230 display. Both graphical and command line interfaces are very easy
233 Type ',list' to list all available commands together with a summary.
234 Type ',help <command>' to get more help about a specific command.
236 Hint: To erase a note, make it empty.
238 tags
= automatic_tags()
240 if command
is not None:
241 method_name
= "_cmd_%s" % command
.lower()
242 if not hasattr(self
, method_name
):
243 raise Exception, "Unknown command: %s" % command
244 method
= getattr(self
, method_name
)
245 note
= textwrap
.dedent(method
.__doc
__).strip() + "\n"
246 self
._enter
(None, note
, tags
, True)
249 def _cmd_list(self
, row_id
, tags
):
251 List all available commands.
253 note
= ["List of Commands", ""]
254 for names
, method
in self
._command
_list
:
255 note
.append(" ".join(",%s" % n
for n
in names
))
256 note
[-1] += " - " + method
.__doc
__.strip().split("\n", 1)[0]
257 note
= "\n".join(note
) + "\n"
258 tags
= automatic_tags()
259 self
._enter
(None, note
, tags
, True)
262 def _cmd_multiline(self
, row_id
, tags
):
264 Convert a note to a multi-line note by appending a line break.
266 row_id
, note
, tags
= self
.jotdb
.get_item(row_id
)
268 self
._enter
(row_id
, note
, tags
, "append")
269 _cmd_m
= _cmd_multiline
271 def _cmd_new(self
, row_id
, tags
):
275 self
._enter
(None, "", tags
)
278 def _cmd_quit(self
, row_id
, tags
):
280 Quit Jot immediately.
285 def _cmd_tag(self
, row_id
, tags
):
289 A new note will be created with a tag that references the original note.
290 Tags are separated by whitespace (blanks, end-of-lines, etc.). Erasing
291 the text of the new note will just erase the new note, not remove all
292 tags of the referenced note. To remove all tags of the referenced note,
293 enter a space before hitting "Jot".
295 row_id
, note
, tags
= self
.jotdb
.get_item(row_id
)
296 note
= "\n".join(tags
.split()) or " "
297 tags
= automatic_tags() + " #tags-of#%u" % row_id
298 self
._enter
(None, note
, tags
, "append")
302 dic
= collections
.defaultdict(list)
304 if not k
.startswith("_cmd_"): continue
305 dic
[loc
[k
]].append(k
[5:])
307 for fn
, n
in dic
.iteritems():
308 _command_list
.append((sorted(n
), fn
))
310 del dic
, fn
, k
, loc
, n
313 ## GRAPHICAL USER INTERFACE
315 class CommandExecGUI(CommandExec
):
317 def __init__(self
, main_window
):
318 super(CommandExecGUI
, self
).__init
__(main_window
.jotdb
)
319 self
.main_window
= main_window
322 self
.main_window
.save_window_state()
323 self
.main_window
.destroy()
325 def _cb_refresh_view(self
):
326 self
.main_window
.refresh_jotdb_model()
328 def _cb_show_row(self
, row_id
, how
):
329 mwin
= self
.main_window
330 sel
= mwin
.jotdb_view
.get_selection()
331 for row
in mwin
.jotdb_model
:
333 sel
.select_path(row
.path
)
334 gobject
.idle_add(mwin
.jot_widget_grab_focus
, how
)
336 raise Exception, "invalid row id"
338 class MainWindow(Window
):
340 def __init__(self
, jotdb
, cache_path
):
341 super(MainWindow
, self
).__init
__()
342 self
.set_default_size(800, 600)
343 self
.connect("delete-event", self
.delete_event
)
344 self
.connect("window-state-event", self
.window_state_event
)
347 self
.cmdexec
= CommandExecGUI(self
)
348 self
.set_title("%s - Jot" % os
.path
.basename(self
.jotdb
.filename
))
349 self
.cache_path
= cache_path
351 geom
= self
.jotdb
.get_config("window-geometry")
352 if geom
: self
.parse_geometry(geom
)
353 window_state
= int(self
.jotdb
.get_config("window-state") or "0")
354 self
.is_maximized
= False
355 if window_state
== 1:
357 self
.is_maximized
= True
359 outer_vbox
, inner_vbox
= gnome_hig(self
)[1:]
360 self
.paned
= gnome_hig(inner_vbox(VPaned(), True, True))
362 # The model contains these fields:
363 # row_id, note, tags, info, note_display
364 self
.jotdb_model
= gtk
.ListStore(int, str, str, str, str)
365 sw
= gnome_hig(self
.paned(ScrolledWindow(), True, False))
366 self
.jotdb_view
= gnome_hig(sw(TreeView(self
.jotdb_model
)))
367 self
.jotdb_view
.connect("row-activated", self
.jot_row_activated
)
368 jot_selection
= self
.jotdb_view
.get_selection()
369 jot_selection
.connect("changed", self
.jot_selection_changed
)
370 cellr
= gtk
.CellRendererText()
372 col
= gtk
.TreeViewColumn("Info", cellr
, text
=3)
373 col
.set_resizable(True)
374 width
= self
.jotdb
.get_config("info-column-width")
377 col
.set_fixed_width(width
)
378 col
.set_sizing(gtk
.TREE_VIEW_COLUMN_FIXED
)
379 self
.jotdb_view
.append_column(col
)
381 col
= gtk
.TreeViewColumn("Note", cellr
, text
=4)
382 col
.set_resizable(True)
383 self
.jotdb_view
.append_column(col
)
385 jot_box
= self
.paned(VBox(), False, False)
386 self
.jot_single
= gnome_hig(jot_box(HBox(), False, False))
387 self
.jot_entry
= self
.jot_single(Entry(), True, True)
388 self
.jot_entry
.connect("key-press-event", self
.jot_key_press
)
389 self
.jot_widget
= self
.jot_entry
390 self
.jot_single_button
= DefaultButton("_Jot")
391 self
.jot_single(self
.jot_single_button
, False, False)
392 self
.jot_single_button
.connect("clicked", self
.jot_clicked
)
394 self
.jot_multi
= gnome_hig(jot_box(VBox(auto_show
=False), True, True))
395 sw
= gnome_hig(self
.jot_multi(ScrolledWindow(), True, True))
396 self
.jot_textview
= gnome_hig(sw(TextViewForCode()))
397 self
.jot_textview
.connect("key-press-event", self
.jot_key_press
)
398 self
.jot_multi_button
= DefaultButton("_Jot", auto_default
=False)
399 self
.jot_multi(self
.jot_multi_button
, False, False)
400 self
.jot_multi_button
.connect("clicked", self
.jot_clicked
)
402 gobject
.idle_add(self
.jot_widget_grab_focus
)
403 self
.jot_single
.paned_position
= 0
404 pos
= self
.jotdb
.get_config("single-paned-position")
406 self
.jot_single
.paned_position
= int(pos
)
407 self
.paned_set_position(int(pos
))
408 self
.jot_multi
.paned_position
= 300
409 pos
= self
.jotdb
.get_config("multi-paned-position")
411 self
.jot_multi
.paned_position
= int(pos
)
413 self
.refresh_jotdb_model()
415 # Window and Paned state management
417 def delete_event(self
, window
, event
):
418 self
.save_window_state()
421 def save_window_state(self
):
422 geom
= "%ux%u+%u+%u" % (self
.get_size() + self
.get_position())
423 self
.jotdb
.set_config("window-geometry", geom
)
424 window_state
= int(self
.is_maximized
)
425 self
.jotdb
.set_config("window-state", str(window_state
))
426 width
= self
.jotdb_view
.get_column(0).get_width()
427 self
.jotdb
.set_config("info-column-width", str(width
))
428 pos
= self
.jot_single
.paned_position
429 self
.jotdb
.set_config("single-paned-position", str(pos
))
430 pos
= self
.jot_multi
.paned_position
431 self
.jotdb
.set_config("multi-paned-position", str(pos
))
433 def window_state_event(self
, window
, event
):
434 if event
.changed_mask
& gtk
.gdk
.WINDOW_STATE_MAXIMIZED
:
435 self
.is_maximized
= bool(event
.new_window_state
&
436 gtk
.gdk
.WINDOW_STATE_MAXIMIZED
)
439 def paned_get_position(self
):
440 return self
.get_size()[1] - self
.paned
.get_position()
442 def paned_set_position(self
, pos
):
443 self
.paned
.set_position(self
.get_size()[1] - pos
)
445 # Jot single / multi / widget
447 def set_jot_is_multiline(self
, is_multiline
):
448 single_visible
= self
.jot_single
.get_property("visible")
449 if not (is_multiline ^ single_visible
):
450 if not is_multiline
: # single-line
451 self
.jot_multi
.paned_position
= self
.paned_get_position()
452 self
.jot_multi
.hide()
453 self
.jot_single
.show()
454 self
.jot_widget
= self
.jot_entry
455 self
.paned_set_position(self
.jot_single
.paned_position
)
456 self
.jot_single_button
.grab_default()
458 self
.jot_single
.paned_position
= self
.paned_get_position()
459 self
.jot_single
.hide()
460 self
.jot_multi
.show()
461 self
.jot_widget
= self
.jot_textview
462 self
.paned_set_position(self
.jot_multi
.paned_position
)
463 self
.jot_multi_button
.grab_default()
466 def jot_widget_grab_focus(self
, how
=True):
467 self
.jot_widget
.grab_focus()
468 if self
.jot_single
.get_property("visible"): # single-line
470 self
.jot_widget
.set_position(-1)
472 buf
= self
.jot_widget
.get_buffer()
473 start
, end
= buf
.get_start_iter(), buf
.get_end_iter()
475 buf
.select_range(end
, end
)
477 buf
.select_range(start
, end
)
478 self
.jot_widget
.scroll_to_iter(buf
.get_end_iter(), 0.0)
480 def jot_key_press(self
, widget
, event
):
481 if KeyPressEval("CONTROL, Return")(event
):
482 self
.jot_single_button
.clicked()
486 def jot_widget_get_text(self
):
487 if self
.jot_single
.get_property("visible"): # single-line
488 return self
.jot_entry
.get_text()
490 return self
.jot_textview
.get_buffer()()
492 def jot_widget_set_text(self
, text
):
493 if not self
.set_jot_is_multiline("\n" in text
): # single-line
494 self
.jot_entry
.set_text(text
)
496 self
.jot_textview
.get_buffer().set_text(text
)
500 def jot_clicked(self
, button
):
501 if self
.row_id
is None:
502 tags
= automatic_tags()
504 row_id
, note
, tags
= self
.jotdb
.get_item(self
.row_id
)
505 note
= self
.jot_widget_get_text()
506 self
.jot_widget_set_text("")
507 self
.cmdexec
.enter(self
.row_id
, note
, tags
)
509 # Jot DB model and view
511 def jot_row_activated(self
, treeview
, path
, view_column
):
512 self
.jot_widget_grab_focus()
514 def jot_selection_changed(self
, jot_selection
):
515 it
= jot_selection
.get_selected()[1]
519 row
= self
.jotdb_model
[it
]
522 if self
.row_id
is not None:
524 self
.jot_widget_set_text(note
)
526 gobject
.idle_add(self
.jot_widget_grab_focus
)
529 def refresh_jotdb_model(self
):
531 self
.jotdb_model
.clear()
532 for row_id
, note
, tags
in self
.jotdb
.get_list():
533 atags
= get_automatic_tags(tags
)
534 info
= (atags
.get("time") or [""])[0]
537 note_display
= "[+] %s" % note
.split("\n", 1)[0]
538 self
.jotdb_model
.append((row_id
, note
, tags
, info
, note_display
))
539 if len(self
.jotdb_model
):
540 self
.jotdb_view
.scroll_to_cell(self
.jotdb_model
[-1].path
)
543 ## COMMAND LINE INTERFACE
545 class CommandExecCLI(CommandExec
):
550 class OptionParser(optparse
.OptionParser
):
552 def __init__(self
, *a
, **kw
):
553 optparse
.OptionParser
.__init
__(self
, *a
, **kw
)
554 self
.add_option("-f", "--jot-file",
555 help="Jot file file to use")
556 self
.add_option("-F", "--file-chooser", action
="store_true",
557 help="Show file chooser dialog to select Jot file")
561 option_parser
= OptionParser()
562 options
, args
= option_parser
.parse_args(argv
[1:])
564 cache
= os
.environ
.get("XDG_CACHE_HOME", os
.path
.expanduser("~/.cache"))
565 cache
= os
.path
.join(cache
, "jot")
566 if not os
.path
.isdir(cache
):
567 os
.makedirs(cache
, 0700)
568 filename_cache
= os
.path
.join(cache
, "last-file.txt")
570 filename
= options
.jot_file
571 if not filename
and os
.path
.isfile(filename_cache
):
572 filename
= file(filename_cache
).readline().rstrip()
573 if not options
.jot_file
:
574 if options
.file_chooser
or not (filename
and os
.path
.isfile(filename
)):
575 fcd
= FileChooserDialogStandard()
576 if filename
: fcd
.set_filename(filename
)
577 if fcd
.run() != gtk
.RESPONSE_ACCEPT
:
579 filename
= fcd
.get_filename()
580 filename
= os
.path
.abspath(filename
)
582 jotdb
= JotDB(filename
)
583 print >>file(filename_cache
, "w"), filename
585 cmdexec
= CommandExecCLI(jotdb
)
586 if not jotdb
.get_count() and not jotdb
.get_config("help-hint-added"):
587 jotdb
.set_config("help-hint-added", True)
588 note
= "Type ',help' (comma + help) to get help"
589 tags
= automatic_tags()
590 jotdb
.enter(None, note
, tags
)
593 note
= " ".join(args
)
594 tags
= automatic_tags()
595 cmdexec
.enter(None, note
, tags
)
597 MainWindow(jotdb
, cache
)
601 if __name__
== "__main__":