"Fossies" - the Fresh Open Source Software Archive  

Source code changes of the file "src/revelation.py" between
revelation-0.5.3.tar.xz and revelation-0.5.4.tar.xz

About: Revelation is a password manager for the GNOME 3 desktop.

revelation.py  (revelation-0.5.3.tar.xz):revelation.py  (revelation-0.5.4.tar.xz)
skipping to change at line 51 skipping to change at line 51
gettext.bindtextdomain(config.PACKAGE, config.DIR_LOCALE) gettext.bindtextdomain(config.PACKAGE, config.DIR_LOCALE)
gettext.textdomain(config.PACKAGE) gettext.textdomain(config.PACKAGE)
# Gtk.Builder uses the C lib's locale API, accessible with the locale mo dule # Gtk.Builder uses the C lib's locale API, accessible with the locale mo dule
locale.bindtextdomain(config.PACKAGE, config.DIR_LOCALE) locale.bindtextdomain(config.PACKAGE, config.DIR_LOCALE)
locale.bind_textdomain_codeset(config.PACKAGE, "UTF-8") locale.bind_textdomain_codeset(config.PACKAGE, "UTF-8")
ui.App.__init__(self, config.APPNAME) ui.App.__init__(self, config.APPNAME)
self.window = None self.window = None
resource_path = os.path.join(config.DIR_UI, 'revelation.gresource')
resource = Gio.Resource.load(resource_path)
resource._register()
def do_startup(self): def do_startup(self):
Gtk.Application.do_startup(self) Gtk.Application.do_startup(self)
if not self.window: if not self.window:
self.window = ui.AppWindow(application=self, title="Revelation") self.window = ui.AppWindow(application=self, title="Revelation")
self.main_vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL,5) self.main_vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL,5)
self.window.add(self.main_vbox) self.window.add(self.main_vbox)
self.add_window(self.window) self.add_window(self.window)
def do_activate(self): def do_activate(self):
self.builder = Gtk.Builder() self.builder = Gtk.Builder()
self.builder.add_from_file(config.DIR_UI + '/menubar.ui') self.builder.add_from_resource('/info/olasagasti/revelation/ui/menubar.u i')
self.menubar = self.builder.get_object("menubar") self.menubar = self.builder.get_object("menubar")
self.popupbuilder = Gtk.Builder() self.popupbuilder = Gtk.Builder()
self.popupbuilder.add_from_file(config.DIR_UI + '/popup-tree.ui') self.popupbuilder.add_from_resource('/info/olasagasti/revelation/ui/popu p-tree.ui')
self.popupmenu = self.popupbuilder.get_object("popup-tree") self.popupmenu = self.popupbuilder.get_object("popup-tree")
self.window.connect("delete-event", self.__cb_quit) self.window.connect("delete-event", self.__cb_quit)
try: try:
self.__init_config() self.__init_config()
self.__init_actions() self.__init_actions()
self.__init_facilities() self.__init_facilities()
self.__init_ui() self.__init_ui()
self.__init_states() self.__init_states()
skipping to change at line 228 skipping to change at line 232
# global action group # global action group
group = Gio.SimpleActionGroup() group = Gio.SimpleActionGroup()
self.window.insert_action_group("file", group) self.window.insert_action_group("file", group)
action = Gio.SimpleAction.new("file-change-password", None) action = Gio.SimpleAction.new("file-change-password", None)
action.connect("activate", lambda w,k: self.file_change_password()) action.connect("activate", lambda w,k: self.file_change_password())
group.add_action(action) group.add_action(action)
action = Gio.SimpleAction.new("file-close", None) action = Gio.SimpleAction.new("file-close", None)
action.connect("activate", self.__cb_quit) action.connect("activate", self.__cb_close)
group.add_action(action) group.add_action(action)
action = Gio.SimpleAction.new("file-export", None) action = Gio.SimpleAction.new("file-export", None)
action.connect("activate", lambda w,k: self.file_export()) action.connect("activate", lambda w,k: self.file_export())
group.add_action(action) group.add_action(action)
action = Gio.SimpleAction.new("file-import", None) action = Gio.SimpleAction.new("file-import", None)
action.connect("activate", lambda w,k: self.file_import()) action.connect("activate", lambda w,k: self.file_import())
group.add_action(action) group.add_action(action)
skipping to change at line 469 skipping to change at line 473
toolbar.insert(editentry_item, -1) toolbar.insert(editentry_item, -1)
removeentry_item = Gtk.ToolButton.new(Gtk.Image.new_from_icon_name('edit -delete', Gtk.IconSize.LARGE_TOOLBAR), _('Re_move')) removeentry_item = Gtk.ToolButton.new(Gtk.Image.new_from_icon_name('edit -delete', Gtk.IconSize.LARGE_TOOLBAR), _('Re_move'))
removeentry_item.connect('clicked', lambda k: self.window.get_action_gro up("entry-multiple").lookup_action("entry-remove").activate()) removeentry_item.connect('clicked', lambda k: self.window.get_action_gro up("entry-multiple").lookup_action("entry-remove").activate())
removeentry_item.set_tooltip_text(_('Remove the selected entries')) removeentry_item.set_tooltip_text(_('Remove the selected entries'))
removeentry_item.set_use_underline(True) removeentry_item.set_use_underline(True)
toolbar.insert(removeentry_item, -1) toolbar.insert(removeentry_item, -1)
self.toolbar=toolbar self.toolbar=toolbar
self.toolbar.connect("popup-context-menu", lambda w,x,y,b: True) self.toolbar.connect("popup-context-menu", lambda w,x,y,b: True)
self.add_toolbar(toolbar, "toolbar", 1, False) self.add_toolbar(toolbar, "toolbar", 1)
self.statusbar = ui.Statusbar() self.statusbar = ui.Statusbar()
self.main_vbox.pack_end(self.statusbar, False, True, 0) self.main_vbox.pack_end(self.statusbar, False, True, 0)
try:
detachable = Gio.Settings.new("org.gnome.desktop.interface").get_boo
lean("toolbar-detachable")
except config.ConfigError:
detachable = False
self.searchbar = ui.Searchbar() self.searchbar = ui.Searchbar()
self.add_toolbar(self.searchbar, "searchbar", 2, detachable) self.add_toolbar(self.searchbar, "searchbar", 2)
# set up main application widgets # set up main application widgets
self.tree = ui.EntryTree(self.entrystore) self.tree = ui.EntryTree(self.entrystore)
self.scrolledwindow = ui.ScrolledWindow(self.tree) self.scrolledwindow = ui.ScrolledWindow(self.tree)
self.entryview = ui.EntryView(self.config, self.clipboard) self.entryview = ui.EntryView(self.config, self.clipboard)
self.entryview.set_halign(Gtk.Align.CENTER) self.entryview.set_halign(Gtk.Align.CENTER)
self.entryview.set_valign(Gtk.Align.CENTER) self.entryview.set_valign(Gtk.Align.CENTER)
self.entryview.set_hexpand(True) self.entryview.set_hexpand(True)
self.hpaned = ui.HPaned(self.scrolledwindow, self.entryview) self.hpaned = Gtk.Paned.new(Gtk.Orientation.HORIZONTAL)
self.hpaned.pack1(self.scrolledwindow, True, True)
self.hpaned.pack2(self.entryview, True, True)
self.hpaned.set_border_width(6)
self.set_contents(self.hpaned) self.set_contents(self.hpaned)
# set up drag-and-drop # set up drag-and-drop
uritarget = Gtk.TargetEntry.new("text/uri-list", 0, 0) uritarget = Gtk.TargetEntry.new("text/uri-list", 0, 0)
self.window.drag_dest_set(Gtk.DestDefaults.ALL, [uritarget], Gdk.DragAct ion.COPY | Gdk.DragAction.MOVE | Gdk.DragAction.LINK ) self.window.drag_dest_set(Gtk.DestDefaults.ALL, [uritarget], Gdk.DragAct ion.COPY | Gdk.DragAction.MOVE | Gdk.DragAction.LINK )
self.window.connect("drag_data_received", self.__cb_drag_dest) self.window.connect("drag_data_received", self.__cb_drag_dest)
self.tree.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, ( ( "r evelation/treerow", Gtk.TargetFlags.SAME_APP | Gtk.TargetFlags.SAME_WIDGET, 0), ), Gdk.DragAction.MOVE) self.tree.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, ( ( "r evelation/treerow", Gtk.TargetFlags.SAME_APP | Gtk.TargetFlags.SAME_WIDGET, 0), ), Gdk.DragAction.MOVE)
self.tree.enable_model_drag_dest(( ( "revelation/treerow", Gtk.TargetFla gs.SAME_APP | Gtk.TargetFlags.SAME_WIDGET, 0), ), Gdk.DragAction.MOVE) self.tree.enable_model_drag_dest(( ( "revelation/treerow", Gtk.TargetFla gs.SAME_APP | Gtk.TargetFlags.SAME_WIDGET, 0), ), Gdk.DragAction.MOVE)
self.tree.connect("drag_data_received", self.__cb_tree_drag_received) self.tree.connect("drag_data_received", self.__cb_tree_drag_received)
# set up callbacks # set up callbacks
self.searchbar.connect("key-press-event", self.__cb_searchbar_key_press) self.searchbar.entry.connect("key-press-event", self.__cb_searchbar_key_ press)
self.searchbar.button_next.connect("clicked", self.__cb_searchbar_button _clicked, data.SEARCH_NEXT) self.searchbar.button_next.connect("clicked", self.__cb_searchbar_button _clicked, data.SEARCH_NEXT)
self.searchbar.button_prev.connect("clicked", self.__cb_searchbar_button _clicked, data.SEARCH_PREVIOUS) self.searchbar.button_prev.connect("clicked", self.__cb_searchbar_button _clicked, data.SEARCH_PREVIOUS)
self.searchbar.entry.connect("changed", lambda w: self.__state_find(self .searchbar.entry.get_text())) self.searchbar.entry.connect("changed", lambda w: self.__state_find(self .searchbar.entry.get_text()))
self.tree.connect("popup", lambda w,d: self.popup(self.popupmenu, d.butt on, d.time)) self.tree.connect("popup", lambda w,d: self.popup(self.popupmenu, d.butt on, d.time))
self.tree.connect("doubleclick", self.__cb_tree_doubleclick) self.tree.connect("doubleclick", self.__cb_tree_doubleclick)
self.tree.connect("key-press-event", self.__cb_tree_keypress) self.tree.connect("key-press-event", self.__cb_tree_keypress)
self.tree.selection.connect("changed", lambda w: self.entryview.display_ entry(self.entrystore.get_entry(self.tree.get_active()))) self.tree.selection.connect("changed", lambda w: self.entryview.display_ entry(self.entrystore.get_entry(self.tree.get_active())))
self.tree.selection.connect("changed", lambda w: self.__state_entry(self .tree.get_selected())) self.tree.selection.connect("changed", lambda w: self.__state_entry(self .tree.get_selected()))
skipping to change at line 727 skipping to change at line 728
def __cb_quit(self, widget, data = None): def __cb_quit(self, widget, data = None):
"Callback for quit" "Callback for quit"
if self.quit() == False: if self.quit() == False:
return True return True
else: else:
return False return False
def __cb_close(self, widget, data = None):
"Callback for Close"
if self.file_close() == False:
return True
else:
return False
def __cb_searchbar_button_clicked(self, widget, direction = data.SEARCH_NEXT ): def __cb_searchbar_button_clicked(self, widget, direction = data.SEARCH_NEXT ):
"Callback for searchbar button clicks" "Callback for searchbar button clicks"
self.__entry_find(self, self.searchbar.entry.get_text(), self.searchbar. dropdown.get_active_type(), direction) self.__entry_find(self, self.searchbar.entry.get_text(), self.searchbar. dropdown.get_active_type(), direction)
self.searchbar.entry.select_region(0, -1) self.searchbar.entry.select_region(0, -1)
def __cb_searchbar_key_press(self, widget, data): def __cb_searchbar_key_press(self, widget, data):
"Callback for searchbar key presses" "Callback for searchbar key presses"
# escape # escape
if data.keyval == Gdk.KEY_Escape: if data.keyval == Gdk.KEY_Escape:
context = widget.get_style_context()
context.remove_class(Gtk.STYLE_CLASS_ERROR)
self.config.set_boolean("view-searchbar", False) self.config.set_boolean("view-searchbar", False)
def __cb_tree_doubleclick(self, widget, iter): def __cb_tree_doubleclick(self, widget, iter):
"Handles doubleclicks on the tree" "Handles doubleclicks on the tree"
if self.config.get_string("behavior-doubleclick") == "edit": if self.config.get_string("behavior-doubleclick") == "edit":
self.entry_edit(iter) self.entry_edit(iter)
elif self.config.get_string("behavior-doubleclick") == "copy": elif self.config.get_string("behavior-doubleclick") == "copy":
self.clip_chain(self.entrystore.get_entry(iter)) self.clip_chain(self.entrystore.get_entry(iter))
skipping to change at line 999 skipping to change at line 1011
iters.append(iter) iters.append(iter)
self.tree.select(iters[0]) self.tree.select(iters[0])
##### PRIVATE METHODS ##### ##### PRIVATE METHODS #####
def __entry_find(self, parent, string, entrytype, direction = data.SEARCH_NE XT): def __entry_find(self, parent, string, entrytype, direction = data.SEARCH_NE XT):
"Searches for an entry" "Searches for an entry"
match = self.entrysearch.find(string, entrytype, self.tree.get_active(), direction) match = self.entrysearch.find(string, entrytype, self.tree.get_active(), direction)
context = self.searchbar.entry.get_style_context()
if match != None: if match != None:
self.tree.select(match) self.tree.select(match)
self.statusbar.set_status(_('Match found for \'%s\'') % string) self.statusbar.set_status(_('Match found for ā€œ%sā€') % string)
context.remove_class(Gtk.STYLE_CLASS_ERROR)
else: else:
self.statusbar.set_status(_('No match found for \'%s\'') % string) self.statusbar.set_status(_('No match found for ā€œ%sā€') % string)
dialog.Error(parent.window, _('No match found'), _('The string \'%s\ context.add_class(Gtk.STYLE_CLASS_ERROR)
' does not match any entries. Try searching for a different phrase.') % string).
run()
def __file_autosave(self): def __file_autosave(self):
"Autosaves the current file if needed" "Autosaves the current file if needed"
try: try:
if self.datafile.get_file() is None or self.datafile.get_password() is None: if self.datafile.get_file() is None or self.datafile.get_password() is None:
return return
if self.config.get_boolean("file-autosave") == False: if self.config.get_boolean("file-autosave") == False:
return return
skipping to change at line 1108 skipping to change at line 1122
##### PUBLIC METHODS ##### ##### PUBLIC METHODS #####
def about(self): def about(self):
"Displays the about dialog" "Displays the about dialog"
dialog.run_unique(dialog.About, self) dialog.run_unique(dialog.About, self)
def clip_chain(self, e): def clip_chain(self, e):
"Copies all passwords from an entry as a chain" "Copies all passwords from an entry as a chain"
if e == None: if e is None:
return return
secrets = [ field.value for field in e.fields if field.datatype == entry .DATATYPE_PASSWORD and field.value != "" ] secrets = [ field.value for field in e.fields if field.datatype == entry .DATATYPE_PASSWORD and field.value != "" ]
if self.config.get_boolean("clipboard-chain-username") == True and len(s ecrets) > 0 and e.has_field(entry.UsernameField) and e[entry.UsernameField] != " ": if self.config.get_boolean("clipboard-chain-username") == True and len(s ecrets) > 0 and e.has_field(entry.UsernameField) and e[entry.UsernameField] != " ":
secrets.insert(0, e[entry.UsernameField]) secrets.insert(0, e[entry.UsernameField])
if len(secrets) == 0: if len(secrets) == 0:
self.statusbar.set_status(_('Entry has no password to copy')) self.statusbar.set_status(_('Entry has no password to copy'))
skipping to change at line 1160 skipping to change at line 1174
) )
self.__file_autosave() self.__file_autosave()
self.tree.unselect_all() self.tree.unselect_all()
self.statusbar.set_status(_('Entries cut')) self.statusbar.set_status(_('Entries cut'))
def clip_paste(self, entrystore, parent): def clip_paste(self, entrystore, parent):
"Pastes entries from the clipboard" "Pastes entries from the clipboard"
if entrystore == None: if entrystore is None:
return return
parent = self.tree.get_active() parent = self.tree.get_active()
iters = self.entrystore.import_entry(entrystore, None, parent) iters = self.entrystore.import_entry(entrystore, None, parent)
paths = [ self.entrystore.get_path(iter) for iter in iters ] paths = [ self.entrystore.get_path(iter) for iter in iters ]
self.undoqueue.add_action( self.undoqueue.add_action(
_('Paste entries'), self.__cb_undo_paste, self.__cb_redo_paste, _('Paste entries'), self.__cb_undo_paste, self.__cb_redo_paste,
( entrystore, self.entrystore.get_path(parent), paths ) ( entrystore, self.entrystore.get_path(parent), paths )
skipping to change at line 1182 skipping to change at line 1196
if len(iters) > 0: if len(iters) > 0:
self.tree.select(iters[0]) self.tree.select(iters[0])
self.statusbar.set_status(_('Entries pasted')) self.statusbar.set_status(_('Entries pasted'))
def entry_add(self, e = None, parent = None, sibling = None): def entry_add(self, e = None, parent = None, sibling = None):
"Adds an entry" "Adds an entry"
try: try:
if e == None: if e is None:
d = dialog.EntryEdit(self.window, _('Add Entry'), None, self.con fig, self.clipboard) d = dialog.EntryEdit(self.window, _('Add Entry'), None, self.con fig, self.clipboard)
d.set_fieldwidget_data(entry.UsernameField, self.__get_common_us ernames()) d.set_fieldwidget_data(entry.UsernameField, self.__get_common_us ernames())
e = d.run() e = d.run()
iter = self.entrystore.add_entry(e, parent, sibling) iter = self.entrystore.add_entry(e, parent, sibling)
self.undoqueue.add_action( self.undoqueue.add_action(
_('Add entry'), self.__cb_undo_add, self.__cb_redo_add, _('Add entry'), self.__cb_undo_add, self.__cb_redo_add,
( self.entrystore.get_path(iter), e.copy() ) ( self.entrystore.get_path(iter), e.copy() )
) )
skipping to change at line 1205 skipping to change at line 1219
self.tree.select(iter) self.tree.select(iter)
self.statusbar.set_status(_('Entry added')) self.statusbar.set_status(_('Entry added'))
except dialog.CancelError: except dialog.CancelError:
self.statusbar.set_status(_('Add entry cancelled')) self.statusbar.set_status(_('Add entry cancelled'))
def entry_edit(self, iter): def entry_edit(self, iter):
"Edits an entry" "Edits an entry"
try: try:
if iter == None: if iter is None:
return return
e = self.entrystore.get_entry(iter) e = self.entrystore.get_entry(iter)
if type(e) == entry.FolderEntry: if type(e) == entry.FolderEntry:
d = dialog.FolderEdit(self.window, _('Edit Folder'), e) d = dialog.FolderEdit(self.window, _('Edit Folder'), e)
else: else:
d = dialog.EntryEdit(self.window, _('Edit Entry'), e, self.confi g, self.clipboard) d = dialog.EntryEdit(self.window, _('Edit Entry'), e, self.confi g, self.clipboard)
d.set_fieldwidget_data(entry.UsernameField, self.__get_common_us ernames(e)) d.set_fieldwidget_data(entry.UsernameField, self.__get_common_us ernames(e))
skipping to change at line 1243 skipping to change at line 1257
"Searches for an entry" "Searches for an entry"
self.config.set_boolean("view-searchbar", True) self.config.set_boolean("view-searchbar", True)
self.searchbar.entry.select_region(0, -1) self.searchbar.entry.select_region(0, -1)
self.searchbar.entry.grab_focus() self.searchbar.entry.grab_focus()
def entry_folder(self, e = None, parent = None, sibling = None): def entry_folder(self, e = None, parent = None, sibling = None):
"Adds a folder" "Adds a folder"
try: try:
if e == None: if e is None:
e = dialog.FolderEdit(self.window, _('Add folder')).run() e = dialog.FolderEdit(self.window, _('Add folder')).run()
iter = self.entrystore.add_entry(e, parent, sibling) iter = self.entrystore.add_entry(e, parent, sibling)
self.undoqueue.add_action( self.undoqueue.add_action(
_('Add folder'), self.__cb_undo_add, self.__cb_redo_add, _('Add folder'), self.__cb_undo_add, self.__cb_redo_add,
( self.entrystore.get_path(iter), e.copy() ) ( self.entrystore.get_path(iter), e.copy() )
) )
self.__file_autosave() self.__file_autosave()
skipping to change at line 1369 skipping to change at line 1383
self.__file_autosave() self.__file_autosave()
self.statusbar.set_status(_('Entries removed')) self.statusbar.set_status(_('Entries removed'))
except dialog.CancelError: except dialog.CancelError:
self.statusbar.set_status(_('Entry removal cancelled')) self.statusbar.set_status(_('Entry removal cancelled'))
def file_change_password(self, password = None): def file_change_password(self, password = None):
"Changes the password of the current data file" "Changes the password of the current data file"
try: try:
if password == None: if password is None:
password = dialog.PasswordChange(self.window, self.datafile.get_ password()).run() password = dialog.PasswordChange(self.window, self.datafile.get_ password()).run()
self.datafile.set_password(password) self.datafile.set_password(password)
self.entrystore.changed = True self.entrystore.changed = True
self.__file_autosave() self.__file_autosave()
self.statusbar.set_status(_('Password changed')) self.statusbar.set_status(_('Password changed'))
except dialog.CancelError: except dialog.CancelError:
self.statusbar.set_status(_('Password change cancelled')) self.statusbar.set_status(_('Password change cancelled'))
def file_close(self):
"Closes the current file"
try:
if self.entrystore.changed == True and dialog.FileChangesClose(self.
window).run() == True:
if self.file_save(self.datafile.get_file(), self.datafile.get_pa
ssword()) == False:
raise dialog.CancelError
self.clipboard.clear()
self.entryclipboard.clear()
self.entrystore.clear()
self.undoqueue.clear()
self.statusbar.set_status(_('Closed file %s') % self.datafile.get_fi
le())
self.datafile.close()
return True
except dialog.CancelError:
self.statusbar.set_status(_('Close file cancelled'))
return False
def file_export(self): def file_export(self):
"Exports data to a foreign file format" "Exports data to a foreign file format"
try: try:
file, handler = dialog.ExportFileSelector(self.window).run() file, handler = dialog.ExportFileSelector(self.window).run()
datafile = io.DataFile(handler) datafile = io.DataFile(handler)
if datafile.get_handler().encryption == True: if datafile.get_handler().encryption == True:
password = dialog.PasswordSave(self.window, file).run() password = dialog.PasswordSave(self.window, file).run()
skipping to change at line 1544 skipping to change at line 1579
except dialog.CancelError: except dialog.CancelError:
self.statusbar.set_status(_('Open cancelled')) self.statusbar.set_status(_('Open cancelled'))
def file_save(self, file = None, password = None): def file_save(self, file = None, password = None):
"Saves data to a file" "Saves data to a file"
try: try:
if file is None: if file is None:
file = dialog.SaveFileSelector(self.window).run() file = dialog.SaveFileSelector(self.window).run()
if password == None: if password is None:
password = dialog.PasswordSave(self.window, file).run() password = dialog.PasswordSave(self.window, file).run()
self.datafile.save(self.entrystore, file, password) self.datafile.save(self.entrystore, file, password)
self.entrystore.changed = False self.entrystore.changed = False
self.statusbar.set_status(_('Data saved to file %s') % file) self.statusbar.set_status(_('Data saved to file %s') % file)
return True return True
except dialog.CancelError: except dialog.CancelError:
self.statusbar.set_status(_('Save cancelled')) self.statusbar.set_status(_('Save cancelled'))
skipping to change at line 1865 skipping to change at line 1900
"Runs the preference dialog" "Runs the preference dialog"
self.show_all() self.show_all()
# for some reason, Gtk crashes on close-by-escape unless we do this # for some reason, Gtk crashes on close-by-escape unless we do this
self.get_widget_for_response(Gtk.ResponseType.CLOSE).grab_focus() self.get_widget_for_response(Gtk.ResponseType.CLOSE).grab_focus()
self.notebook.grab_focus() self.notebook.grab_focus()
if __name__ == "__main__": if __name__ == "__main__":
app = Revelation() app = Revelation()
app.set_flags(Gio.ApplicationFlags.NON_UNIQUE)
app.run() app.run()
 End of changes. 23 change blocks. 
26 lines changed or deleted 62 lines changed or added

Home  |  About  |  Features  |  All  |  Newest  |  Dox  |  Diffs  |  RSS Feeds  |  Screenshots  |  Comments  |  Imprint  |  Privacy  |  HTTP(S)