Compare commits
5 Commits
275bdb6583
...
1b2e43e2c3
| Author | SHA1 | Date | |
|---|---|---|---|
| 1b2e43e2c3 | |||
| 5815fffcab | |||
| ee7bde5adc | |||
| 6af7b9b1b8 | |||
| b292556403 |
162
gtkpass.py
162
gtkpass.py
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
import signal
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
import gi
|
||||
@@ -27,6 +28,7 @@ class GTKPass(Gtk.Window):
|
||||
self.conf = self.passs.conf
|
||||
self._border = 5
|
||||
self._expand = False
|
||||
self._selected = None
|
||||
self.make_ui()
|
||||
|
||||
def make_ui(self):
|
||||
@@ -77,8 +79,10 @@ class GTKPass(Gtk.Window):
|
||||
self.ts_filter = self.tree_store.filter_new()
|
||||
self.ts_filter.set_visible_column(0)
|
||||
self.treeview = Gtk.TreeView(model=self.ts_filter)
|
||||
self.treeview.set_activate_on_single_click(True)
|
||||
self.treeview.set_headers_visible(False)
|
||||
self.treeview.connect("key-release-event", self.on_treeview_keypress)
|
||||
self.treeview.connect('row-activated', self.on_row_activated)
|
||||
|
||||
icon_renderer = Gtk.CellRendererPixbuf()
|
||||
text_renderer = Gtk.CellRendererText()
|
||||
@@ -168,6 +172,7 @@ class GTKPass(Gtk.Window):
|
||||
|
||||
b_dir = Gtk.ToolButton()
|
||||
b_dir.set_icon_name("folder-new-symbolic")
|
||||
b_dir.connect("clicked", self.on_new_dir)
|
||||
toolbar.insert(b_dir, 1)
|
||||
|
||||
b_edit = Gtk.ToolButton()
|
||||
@@ -176,6 +181,7 @@ class GTKPass(Gtk.Window):
|
||||
|
||||
b_del = Gtk.ToolButton()
|
||||
b_del.set_icon_name("edit-delete-symbolic")
|
||||
b_del.connect("clicked", self.on_delete)
|
||||
toolbar.insert(b_del, 3)
|
||||
|
||||
b_gitpush = Gtk.ToolButton()
|
||||
@@ -250,6 +256,21 @@ class GTKPass(Gtk.Window):
|
||||
self.make_subtree_visible(model, iter)
|
||||
return
|
||||
|
||||
def on_row_activated(self, treeview, treepath, treeview_col):
|
||||
selection = treeview.get_selection()
|
||||
|
||||
if not selection:
|
||||
return
|
||||
|
||||
model, treeiter = selection.get_selected()
|
||||
|
||||
if (self._selected is not None and
|
||||
self._selected == model[treeiter][4]):
|
||||
self._selected = None
|
||||
selection.unselect_all()
|
||||
else:
|
||||
self._selected = model[treeiter][4]
|
||||
|
||||
def on_selected(self, selection):
|
||||
model, treeiter = selection.get_selected()
|
||||
|
||||
@@ -300,6 +321,84 @@ class GTKPass(Gtk.Window):
|
||||
if event.keyval == Gdk.KEY_Left and treeview.get_cursor()[0]:
|
||||
treeview.collapse_row(treeview.get_cursor()[0])
|
||||
|
||||
def on_new_dir(self, button):
|
||||
if self._selected is None:
|
||||
path = ''
|
||||
else:
|
||||
path = self._selected
|
||||
|
||||
dialog = NewDirDialog(self, path)
|
||||
response = dialog.run()
|
||||
dirname = dialog.get_dirname()
|
||||
dialog.destroy()
|
||||
|
||||
if response != Gtk.ResponseType.OK or not dirname:
|
||||
return
|
||||
|
||||
result, msg = self.passs.new_dir(os.path.join(path, dirname))
|
||||
if not result:
|
||||
dialog = Gtk.MessageDialog(transient_for=self,
|
||||
flags=0,
|
||||
message_type=Gtk.MessageType.INFO,
|
||||
buttons=Gtk.ButtonsType.CLOSE,
|
||||
text='There was an error')
|
||||
|
||||
dialog.format_secondary_text(msg)
|
||||
dialog.run()
|
||||
dialog.destroy()
|
||||
|
||||
selection = self.treeview.get_selection()
|
||||
if not selection:
|
||||
return
|
||||
tree_model_filter, tree_paths = selection.get_selected_rows()
|
||||
tree_store = tree_model_filter.get_model()
|
||||
tree_iter = tree_store.get_iter(tree_paths[0])
|
||||
|
||||
self.tree_store.append(tree_iter, [True, dirname,
|
||||
Pango.Weight.NORMAL, "folder",
|
||||
os.path.join(path, dirname), False])
|
||||
|
||||
|
||||
def on_delete(self, button):
|
||||
if not self._selected:
|
||||
return
|
||||
|
||||
# TODO: add configurable confirmation?
|
||||
result, msg = self.passs.delete(self._selected)
|
||||
if result == self.passs.NON_EMPTY:
|
||||
dialog = Gtk.MessageDialog(transient_for=self,
|
||||
flags=0,
|
||||
message_type=Gtk.MessageType.QUESTION,
|
||||
buttons=Gtk.ButtonsType.OK_CANCEL,
|
||||
text='Directory not empty')
|
||||
dialog.format_secondary_text(f'Do you want to delete '
|
||||
f'{self._selected} recursively?')
|
||||
response = dialog.run()
|
||||
dialog.destroy()
|
||||
|
||||
if response == Gtk.ResponseType.OK:
|
||||
result, msg = self.passs.delete(self._selected, True)
|
||||
|
||||
if result == self.passs.ERROR:
|
||||
dialog = Gtk.MessageDialog(transient_for=self,
|
||||
flags=0,
|
||||
message_type=Gtk.MessageType.INFO,
|
||||
buttons=Gtk.ButtonsType.CLOSE,
|
||||
text='There was an error')
|
||||
dialog.format_secondary_text(msg)
|
||||
dialog.run()
|
||||
dialog.destroy()
|
||||
self._selected = None
|
||||
|
||||
# remove selected branch/leaf from store
|
||||
selection = self.treeview.get_selection()
|
||||
tree_model_filter, tree_paths = selection.get_selected_rows()
|
||||
tree_store = tree_model_filter.get_model()
|
||||
|
||||
for path in tree_paths:
|
||||
tree_iter = tree_store.get_iter(path)
|
||||
tree_store.remove(tree_iter)
|
||||
|
||||
def on_key_press_event(self, widget, event):
|
||||
ctrl = (event.state & Gdk.ModifierType.CONTROL_MASK)
|
||||
if ctrl and event.keyval == Gdk.KEY_b:
|
||||
@@ -311,6 +410,32 @@ class GTKPass(Gtk.Window):
|
||||
# TODO: clear clipboard after a minute or so.
|
||||
|
||||
|
||||
class NewDirDialog(Gtk.Dialog):
|
||||
def __init__(self, parent, path):
|
||||
super().__init__(title="Enter new directory", transient_for=parent,
|
||||
flags=0)
|
||||
self.set_modal(True)
|
||||
self.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
|
||||
Gtk.STOCK_OK, Gtk.ResponseType.OK)
|
||||
|
||||
label = Gtk.Label(label=f"Create new directory under "
|
||||
f"{'/' if not path else path} path")
|
||||
|
||||
box = self.get_content_area()
|
||||
box.add(label)
|
||||
self.entry = Gtk.Entry()
|
||||
self.entry.connect("key-release-event", self.on_release_key)
|
||||
box.add(self.entry)
|
||||
self.show_all()
|
||||
|
||||
def on_release_key(self, entry, event):
|
||||
if event.keyval == Gdk.KEY_Return:
|
||||
self.response(Gtk.ResponseType.OK)
|
||||
|
||||
def get_dirname(self):
|
||||
return self.entry.get_text()
|
||||
|
||||
|
||||
class Leaf:
|
||||
"""A simple class to hold Leaf data"""
|
||||
def __init__(self, name, path):
|
||||
@@ -346,6 +471,10 @@ class Tree:
|
||||
|
||||
class PassStore:
|
||||
"""Password store GUI app"""
|
||||
NON_EMPTY = 1
|
||||
SUCCESS = 0
|
||||
ERROR = 2
|
||||
|
||||
def __init__(self):
|
||||
self.store_path = self._get_store_path()
|
||||
self.data = Tree()
|
||||
@@ -363,6 +492,7 @@ class PassStore:
|
||||
return path
|
||||
|
||||
def gather_pass_tree(self):
|
||||
self.data = Tree()
|
||||
self._gather_pass_tree(self.data, self.store_path, '')
|
||||
|
||||
def get_pass(self, path):
|
||||
@@ -373,6 +503,38 @@ class PassStore:
|
||||
else:
|
||||
return False, proc.stderr
|
||||
|
||||
def new_dir(self, dirname):
|
||||
path = os.path.join(self.store_path, dirname)
|
||||
try:
|
||||
os.mkdir(os.path.join('/root', path), mode=500)
|
||||
return True, ''
|
||||
except IOError as exc:
|
||||
return False, str(exc)
|
||||
|
||||
def delete(self, item, recursively=False):
|
||||
path = os.path.join(self.store_path, item)
|
||||
|
||||
if os.path.exists(path) and os.path.isdir(path) and recursively:
|
||||
try:
|
||||
shutil.rmtree(path)
|
||||
except IOError as exc:
|
||||
return self.ERROR, str(exc)
|
||||
elif os.path.exists(path) and os.path.isdir(path):
|
||||
_, files, dirs = next(os.walk(path))
|
||||
if files or dirs:
|
||||
return self.NON_EMPTY, ""
|
||||
try:
|
||||
shutil.rmtree(path)
|
||||
except IOError as exc:
|
||||
return self.ERROR, str(exc)
|
||||
elif not os.path.exists and os.path.isfile(path + '.gpg'):
|
||||
try:
|
||||
os.unlink(path + '.gpg')
|
||||
except IOError as exc:
|
||||
return self.ERROR, str(exc)
|
||||
|
||||
return self.SUCCESS, ''
|
||||
|
||||
def _gather_pass_tree(self, model, root, dirname):
|
||||
fullpath = os.path.join(root, dirname)
|
||||
ps_path = fullpath[len(self.store_path)+1:]
|
||||
|
||||
Reference in New Issue
Block a user