|
@@ -1,4 +1,4 @@
|
|
|
-#!/usr/bin/python
|
|
|
+#!/usr/bin/python3
|
|
|
""" a simple gui or command line app
|
|
|
to view and create/edit file comments
|
|
|
|
|
@@ -14,6 +14,9 @@ where possible, comments are duplicated in
|
|
|
|
|
|
these comments stick to the symlink, not the deref
|
|
|
|
|
|
+nav tools are enabled, so you can double-click to go into a dir, and
|
|
|
+ there's an UP arrow to navigate back
|
|
|
+
|
|
|
"""
|
|
|
|
|
|
|
|
@@ -42,8 +45,9 @@ comment box gets a light yellow background.
|
|
|
|
|
|
import sys,os,argparse,stat
|
|
|
#~ from dirWidget import DirWidget
|
|
|
-from PyQt4.QtGui import *
|
|
|
-from PyQt4 import QtGui, QtCore
|
|
|
+from PyQt5.QtGui import *
|
|
|
+from PyQt5.QtWidgets import *
|
|
|
+from PyQt5.QtCore import Qt
|
|
|
import xattr, sqlite3, time
|
|
|
|
|
|
VERSION = "0.2"
|
|
@@ -52,7 +56,7 @@ DATABASE_NAME = "~/.dirnotes.db"
|
|
|
# convert the ~/ form to a fully qualified path
|
|
|
DATABASE_NAME = os.path.expanduser(DATABASE_NAME)
|
|
|
|
|
|
-class DataBase:
|
|
|
+class dnDataBase:
|
|
|
''' the database is flat
|
|
|
fileName: fully qualified name
|
|
|
st_mtime: a float
|
|
@@ -81,22 +85,22 @@ class DataBase:
|
|
|
c = self.db.cursor()
|
|
|
c.execute("select * from dirnotes where name=? and comment<>'' order by comment_date desc",(os.path.abspath(fileName),))
|
|
|
return c.fetchone()
|
|
|
- def setData(self, fileName, _date, _size, comment):
|
|
|
- c = self.db.cursor()
|
|
|
- c.execute("insert into dirnotes values (?,?,?,?,?)",
|
|
|
- (fileName, _date, _size, comment, time.time()))
|
|
|
- self.db.commit()
|
|
|
- return True
|
|
|
- def log(self, fileName, comment):
|
|
|
+ def setData(self, fileName, comment):
|
|
|
''' TODO: convert filename to canonical '''
|
|
|
c = self.db.cursor()
|
|
|
s = os.lstat(fileName)
|
|
|
- print "params: %s %s %d %s %s" % ((os.path.abspath(fileName),
|
|
|
- DataBase.epochToDb(s.st_mtime), s.st_size, comment,
|
|
|
- DataBase.epochToDb(time.time())))
|
|
|
- c.execute("insert into dirnotes values (?,datetime(?,'unixepoch','localtime'),?,?,datetime(?,'unixepoch','localtime'))",
|
|
|
- (os.path.abspath(fileName), s.st_mtime, s.st_size, str(comment), time.time()))
|
|
|
- self.db.commit()
|
|
|
+ print ("params: %s %s %d %s %s" % ((os.path.abspath(fileName),
|
|
|
+ dnDataBase.epochToDb(s.st_mtime), s.st_size, comment,
|
|
|
+ dnDataBase.epochToDb(time.time()))))
|
|
|
+ try:
|
|
|
+ c.execute("insert into dirnotes values (?,datetime(?,'unixepoch','localtime'),?,?,datetime(?,'unixepoch','localtime'))",
|
|
|
+ (os.path.abspath(fileName), s.st_mtime, s.st_size, str(comment), time.time()))
|
|
|
+ self.db.commit()
|
|
|
+ except sqlite3.OperationalError:
|
|
|
+ print("database is locked or unwriteable")
|
|
|
+ #TODO: put up a message box for locked database
|
|
|
+
|
|
|
+
|
|
|
@staticmethod
|
|
|
def epochToDb(epoch):
|
|
|
return time.strftime("%Y-%m-%d %H:%M:%S",time.localtime(epoch))
|
|
@@ -105,7 +109,7 @@ class DataBase:
|
|
|
return time.mktime(time.strptime(dbTime,"%Y-%m-%d %H:%M:%S"))
|
|
|
@staticmethod
|
|
|
def getShortDate(longDate):
|
|
|
- e = DataBase.DbToEpoch(longDate)
|
|
|
+ e = dnDataBase.DbToEpoch(longDate)
|
|
|
sd = time.strptime(longDate,"%Y-%m-%d %H:%M:%S")
|
|
|
# check for this year, or today
|
|
|
ty = time.mktime((time.localtime()[0],1,1,0,0,0,0,1,-1))
|
|
@@ -118,10 +122,10 @@ class DataBase:
|
|
|
return time.strftime("%X",sd)
|
|
|
#~ test code for this routine
|
|
|
#~ for i in range(int(time.time() - 370*24*3600),
|
|
|
- #~ int(time.time() + 48*3600),
|
|
|
- #~ 3599):
|
|
|
- #~ ds = DataBase.epochToDb(i)
|
|
|
- #~ print("%d\t%s\t%s" % (i,ds,DataBase.getShortDate(ds)))
|
|
|
+ #~ int(time.time() + 48*3600),
|
|
|
+ #~ 3599):
|
|
|
+ #~ ds = dnDataBase.epochToDb(i)
|
|
|
+ #~ print("%d\t%s\t%s" % (i,ds,dnDataBase.getShortDate(ds)))
|
|
|
|
|
|
|
|
|
|
|
@@ -146,29 +150,31 @@ class FileObj():
|
|
|
def __init__(self, fileName):
|
|
|
self.fileName = fileName
|
|
|
s = os.lstat(fileName)
|
|
|
- self.date = DataBase.epochToDb(s.st_mtime)
|
|
|
+ self.date = dnDataBase.epochToDb(s.st_mtime)
|
|
|
if stat.S_ISDIR(s.st_mode):
|
|
|
self.size = FileObj.FILE_IS_DIR
|
|
|
elif stat.S_ISLNK(s.st_mode):
|
|
|
self.size = FileObj.FILE_IS_LINK
|
|
|
else:
|
|
|
self.size = s.st_size
|
|
|
- self.comment = ''
|
|
|
+ self.xattrComment = ''
|
|
|
try:
|
|
|
- self.comment = xattr.get(fileName,COMMENT_KEY,nofollow=True)
|
|
|
+ self.xattrComment = xattr.get(fileName,COMMENT_KEY,nofollow=True).decode()
|
|
|
except Exception as e:
|
|
|
- print("comment read on %s failed, execption %s" % (self.fileName,e))
|
|
|
+ #print("comment read on %s failed, execption %s" % (self.fileName,e))
|
|
|
pass
|
|
|
+
|
|
|
def getName(self):
|
|
|
return self.fileName
|
|
|
def getFileName(self):
|
|
|
return os.path.split(self.fileName)[1]
|
|
|
- def getComment(self):
|
|
|
- return self.comment
|
|
|
- def setComment(self,newComment):
|
|
|
- self.comment = newComment
|
|
|
+
|
|
|
+ def getXattrComment(self):
|
|
|
+ return self.xattrComment
|
|
|
+ def setXattrComment(self,newComment):
|
|
|
+ self.xattrComment = newComment
|
|
|
try:
|
|
|
- xattr.set(self.fileName,COMMENT_KEY,self.comment,nofollow=True)
|
|
|
+ xattr.set(self.fileName,COMMENT_KEY,self.xattrComment,nofollow=True)
|
|
|
return True
|
|
|
# we need to move these cases out to a handler
|
|
|
except Exception as e:
|
|
@@ -184,6 +190,15 @@ class FileObj():
|
|
|
elif "Errno 95" in str(e):
|
|
|
print("is this a VFAT or EXFAT volume? these don't allow comments")
|
|
|
return False
|
|
|
+
|
|
|
+ def getDbComment(self,db):
|
|
|
+ self.dbComment = ""
|
|
|
+ row = db.getData(self.fileName)
|
|
|
+ if row and len(row)>1:
|
|
|
+ self.dbComment = row[3]
|
|
|
+ def setDbComment(self,db,comment):
|
|
|
+ db.setData(self.fileName,comment)
|
|
|
+
|
|
|
def getDate(self):
|
|
|
return self.date
|
|
|
def getSize(self):
|
|
@@ -207,10 +222,12 @@ class HelpWidget(QDialog):
|
|
|
# sortable TableWidgetItem, based on idea by Aledsandar
|
|
|
# http://stackoverflow.com/questions/12673598/python-numerical-sorting-in-qtablewidget
|
|
|
# NOTE: the QTableWidgetItem has setData() and data() which may allow data bonding
|
|
|
+# in Qt5, data() binding is more awkward, so do it here
|
|
|
class SortableTableWidgetItem(QTableWidgetItem):
|
|
|
- def __init__(self, text, sortValue):
|
|
|
+ def __init__(self, text, sortValue, file_object):
|
|
|
QTableWidgetItem.__init__(self, text, QTableWidgetItem.UserType)
|
|
|
self.sortValue = sortValue
|
|
|
+ self.file_object = file_object
|
|
|
def __lt__(self, other):
|
|
|
return self.sortValue < other.sortValue
|
|
|
|
|
@@ -231,7 +248,7 @@ class DirNotes(QMainWindow):
|
|
|
lb = QTableWidget()
|
|
|
self.lb = lb
|
|
|
lb.setColumnCount(4)
|
|
|
- lb.horizontalHeader().setResizeMode( 3, QHeaderView.Stretch );
|
|
|
+ lb.horizontalHeader().setSectionResizeMode( 3, QHeaderView.Stretch );
|
|
|
lb.verticalHeader().setDefaultSectionSize(20); # thinner rows
|
|
|
lb.verticalHeader().setVisible(False)
|
|
|
|
|
@@ -306,7 +323,7 @@ class DirNotes(QMainWindow):
|
|
|
print("closing")
|
|
|
def sbd(self):
|
|
|
print("sort by date")
|
|
|
- self.lb.sortItems(1,QtCore.Qt.DescendingOrder)
|
|
|
+ self.lb.sortItems(1,Qt.DescendingOrder)
|
|
|
def sbs(self):
|
|
|
print("sort by size")
|
|
|
self.lb.sortItems(2)
|
|
@@ -317,12 +334,12 @@ class DirNotes(QMainWindow):
|
|
|
HelpWidget(self)
|
|
|
def sbc(self):
|
|
|
print("sort by comment")
|
|
|
- self.lb.sortItems(3,QtCore.Qt.DescendingOrder)
|
|
|
+ self.lb.sortItems(3,Qt.DescendingOrder)
|
|
|
def newDir(self):
|
|
|
print("change dir to "+self.dirLeft.currentPath())
|
|
|
def double(self,row,col):
|
|
|
print("double click {} {}".format(row, col))
|
|
|
- fo = self.lb.item(row,0).data(32).toPyObject()
|
|
|
+ fo = self.lb.item(row,0).file_object
|
|
|
if col==0 and fo.getSize() == FileObj.FILE_IS_DIR:
|
|
|
print("double click on {}".format(fo.getName()))
|
|
|
self.curPath = fo.getName()
|
|
@@ -339,8 +356,8 @@ class DirNotes(QMainWindow):
|
|
|
dirIcon = QIcon.fromTheme('folder')
|
|
|
fileIcon = QIcon.fromTheme('text-x-generic')
|
|
|
linkIcon = QIcon.fromTheme('emblem-symbolic-link')
|
|
|
- current, dirs, files = os.walk(self.curPath,followlinks=True).next()
|
|
|
- dirs = map(lambda x:x+'/', dirs)
|
|
|
+ current, dirs, files = next(os.walk(self.curPath,followlinks=True))
|
|
|
+ dirs = list(map(lambda x:x+'/', dirs))
|
|
|
dirs.sort()
|
|
|
files.sort()
|
|
|
|
|
@@ -353,40 +370,52 @@ class DirNotes(QMainWindow):
|
|
|
#~ print("insert {} items into cleared table {}".format(len(d),current))
|
|
|
for i in range(len(d)):
|
|
|
this_file = FileObj(current+'/'+d[i])
|
|
|
+ this_file.getDbComment(self.db)
|
|
|
+ print("FileObj created as {} and the db-comment is <{}>".format(this_file.fileName, this_file.dbComment))
|
|
|
#~ print("insert order check: {} {} {} {}".format(d[i],i,this_file.getName(),this_file.getDate()))
|
|
|
#~ self.files.update({this_file.getName(),this_file})
|
|
|
self.files = self.files + [this_file]
|
|
|
if this_file.getSize() == FileObj.FILE_IS_DIR:
|
|
|
- item = SortableTableWidgetItem(d[i],' '+d[i]) # directories sort first
|
|
|
+ item = SortableTableWidgetItem(d[i],' '+d[i], this_file) # directories sort first
|
|
|
else:
|
|
|
- item = SortableTableWidgetItem(d[i],d[i])
|
|
|
- item.setFlags(QtCore.Qt.ItemIsEnabled)
|
|
|
- item.setData(32,this_file) # keep a hidden copy of the file object
|
|
|
+ item = SortableTableWidgetItem(d[i],d[i], this_file)
|
|
|
+ item.setFlags(Qt.ItemIsEnabled)
|
|
|
+ #item.setData(32,this_file) # keep a hidden copy of the file object
|
|
|
item.setToolTip(this_file.getName())
|
|
|
self.lb.setItem(i,0,item)
|
|
|
#lb.itemAt(i,0).setFlags(Qt.ItemIsEnabled) #NoItemFlags)
|
|
|
- comment = this_file.getComment()
|
|
|
+
|
|
|
+ # get the comment from database & xattrs, either can fail
|
|
|
+ comment = ""
|
|
|
+ row = self.db.getData(this_file.getName())
|
|
|
+ if row and len(row)>0:
|
|
|
+ comment = row[3]
|
|
|
+ xattrComment = this_file.getXattrComment()
|
|
|
self.lb.setItem(i,3,QTableWidgetItem(comment))
|
|
|
+ if xattrComment != comment:
|
|
|
+ self.lb.item(i,3).setBackground(QBrush(Qt.yellow))
|
|
|
+ print("got two comments <{}> and <{}>".format(comment, xattrComment))
|
|
|
+
|
|
|
dt = this_file.getDate()
|
|
|
- da = SortableTableWidgetItem(DataBase.getShortDate(dt),dt)
|
|
|
+ da = SortableTableWidgetItem(dnDataBase.getShortDate(dt),dt,this_file)
|
|
|
#da.setFont(small_font)
|
|
|
da.setToolTip(dt)
|
|
|
- da.setFlags(QtCore.Qt.ItemIsEnabled)
|
|
|
+ da.setFlags(Qt.ItemIsEnabled)
|
|
|
self.lb.setItem(i,1,da)
|
|
|
si = this_file.getSize()
|
|
|
if si==FileObj.FILE_IS_DIR:
|
|
|
- sa = SortableTableWidgetItem('',0)
|
|
|
+ sa = SortableTableWidgetItem('',0,this_file)
|
|
|
item.setIcon(dirIcon)
|
|
|
elif si==FileObj.FILE_IS_LINK:
|
|
|
- sa = SortableTableWidgetItem('symlink',-1)
|
|
|
+ sa = SortableTableWidgetItem('symlink',-1,this_file)
|
|
|
item.setIcon(linkIcon)
|
|
|
dst = os.path.realpath(this_file.getName())
|
|
|
sa.setToolTip("symlink: " + dst)
|
|
|
else:
|
|
|
- sa = SortableTableWidgetItem(str(si),si)
|
|
|
+ sa = SortableTableWidgetItem(str(si),si,this_file)
|
|
|
item.setIcon(fileIcon)
|
|
|
- sa.setTextAlignment(QtCore.Qt.AlignRight)
|
|
|
- sa.setFlags(QtCore.Qt.ItemIsEnabled)
|
|
|
+ sa.setTextAlignment(Qt.AlignRight)
|
|
|
+ sa.setFlags(Qt.ItemIsEnabled)
|
|
|
self.lb.setItem(i,2,sa)
|
|
|
self.refilling = False
|
|
|
self.lb.sortingEnabled = True
|
|
@@ -396,16 +425,17 @@ class DirNotes(QMainWindow):
|
|
|
if self.refilling:
|
|
|
return
|
|
|
print("debugging " + x.text() + " r:" + str(x.row()) + " c:" + str(x.column()))
|
|
|
- print(" selected file: "+self.lb.item(x.row(),0).data(32).toPyObject().getName())
|
|
|
- the_file = self.lb.item(x.row(),0).data(32).toPyObject()
|
|
|
+ print(" selected file: "+self.lb.item(x.row(),0).file_object.getName())
|
|
|
+ the_file = self.lb.item(x.row(),0).file_object
|
|
|
print(" and the row file is "+the_file.getName())
|
|
|
- r = the_file.setComment(str(x.text())) # TODO: change priority
|
|
|
+ r = the_file.setXattrComment(str(x.text())) # TODO: change priority
|
|
|
if r:
|
|
|
- self.db.log(the_file.getName(),x.text())
|
|
|
+ self.db.setData(the_file.getName(),x.text())
|
|
|
+
|
|
|
def restore_from_database(self):
|
|
|
print("restore from database")
|
|
|
# retrieve the full path name
|
|
|
- fileName = str(self.lb.item(self.lb.currentRow(),0).data(32).toPyObject().getName())
|
|
|
+ fileName = str(self.lb.item(self.lb.currentRow(),0).file_object.getName())
|
|
|
print("using filename: "+fileName)
|
|
|
existing_comment = str(self.lb.item(self.lb.currentRow(),3).text())
|
|
|
print("restore....existing="+existing_comment+"=")
|
|
@@ -431,8 +461,9 @@ if __name__=="__main__":
|
|
|
p.dirname = p.dirname + '/'
|
|
|
print(p.dirname)
|
|
|
|
|
|
- db = DataBase()
|
|
|
+ db = dnDataBase()
|
|
|
|
|
|
+ # TODO: if there's a file specified, jump to it
|
|
|
a = QApplication([])
|
|
|
dn = DirNotes(p.dirname,db)
|
|
|
dn.show()
|
|
@@ -471,7 +502,4 @@ user.xdg.publisher
|
|
|
''' commandline xattr
|
|
|
getfattr -h (don't follow symlink) -d (dump all properties)
|
|
|
'''
|
|
|
-''' qt set color
|
|
|
- newitem.setData(Qt.BackgroundRole,QBrush(QColor("yellow")))
|
|
|
-'''
|
|
|
''' if the args line contains a file, jump to it '''
|