Compare commits

...

5 Commits

Author SHA1 Message Date
1b2e43e2c3 Remove recreate tree method.
Removing and recreating tree isn't very efficient, so for operations
like adding new directory or removing items from the store, only
affected rows are changed (added or removed from the treestore).
2023-10-10 18:11:07 +02:00
5815fffcab Handle deleting things 2023-10-10 18:06:22 +02:00
ee7bde5adc Make an empty Tree() as a base for recreating tree. 2023-10-10 18:05:16 +02:00
6af7b9b1b8 Added delete method.
This probably will be reiterate later, as password store need to be
updated as well (i.e. removing changes need to be committed in git).
2023-10-10 18:03:34 +02:00
b292556403 Attempt to create directory on the repository. 2023-10-10 17:58:03 +02:00

View File

@@ -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:]