|
@@ -1,10 +1,8 @@
|
|
|
#!/usr/bin/python3
|
|
|
-# TODO: get rid of sqlite cursors; execute on connection
|
|
|
-# TODO: add index to table creation
|
|
|
-# TODO: why are there TWO sqlite.connect()??
|
|
|
-# try auto-commit (isolation_level = "IMMEDIATE")
|
|
|
-# try dict-parameters in sql statements
|
|
|
-# TODO: pick up comment for cwd and display at the top somewhere, or maybe status line
|
|
|
+
|
|
|
+# TODO: XDG compatibility: config in ~/.config/dirnotes/dirnotes.conf and data in ~/.local/share/dirnotes/dirnotes.db
|
|
|
+# TODO: clearing a comment out to '' doesn't seem to work on both sides
|
|
|
+
|
|
|
""" a simple gui or command line app
|
|
|
to view and create/edit file comments
|
|
|
|
|
@@ -23,7 +21,7 @@ nav tools are enabled, so you can double-click to go into a dir
|
|
|
|
|
|
"""
|
|
|
|
|
|
-VERSION = "0.5"
|
|
|
+VERSION = "0.8"
|
|
|
|
|
|
helpMsg = f"""<table width=100%><tr><td><h1>Dirnotes</h1><td>
|
|
|
<td align=right>Version: {VERSION}</td></tr></table>
|
|
@@ -40,8 +38,8 @@ field of the file system.
|
|
|
<p> Double click on directory names to navigate the file system. Hover over
|
|
|
fields for more information.
|
|
|
|
|
|
-<h3>xattr extended attributes</h3>
|
|
|
-The xattr comment suffers from a few problems:
|
|
|
+<h4>xattr extended attributes</h4>
|
|
|
+The <i>xattr</i> comment suffers from a few problems:
|
|
|
<ul>
|
|
|
<li>is not implemented on FAT/VFAT/EXFAT file systems (some USB sticks)
|
|
|
<li>xattrs are not (by default) copied with the file when it's duplicated
|
|
@@ -53,17 +51,22 @@ or backedup (<i>mv, rsync</i> and <i>tar</i> work, <i>ssh</i> and <i>scp</i> don
|
|
|
On the other hand, <i>xattr</i> comments can be bound directly to files on removable
|
|
|
media (as long as the disk format allows it).
|
|
|
|
|
|
+<h4>database comments</h4>
|
|
|
+<p><i>Database</i> comments can be applied to any file, even read-only files and executables.
|
|
|
+
|
|
|
<p>When the <i>database</i> version of a comment differs from the <i>xattr</i> version,
|
|
|
the comment box gets a light yellow background.
|
|
|
+
|
|
|
+<p>To edit a comment, first select it; to replace the comment, just type over it; to edit the comment,
|
|
|
+double-click the mouse, or hit <Enter>.
|
|
|
"""
|
|
|
|
|
|
-import sys,os,argparse,stat,getpass,shutil
|
|
|
-from PyQt5.QtGui import *
|
|
|
-from PyQt5.QtWidgets import *
|
|
|
-from PyQt5.QtCore import Qt, pyqtSignal
|
|
|
-import sqlite3, json, time
|
|
|
+import sys,os,time,argparse,stat,getpass,shutil,logging,math
|
|
|
+from PyQt5.QtGui import *
|
|
|
+from PyQt5.QtWidgets import *
|
|
|
+from PyQt5.QtCore import Qt, pyqtSignal
|
|
|
+import sqlite3, json
|
|
|
|
|
|
-VERSION = "0.4"
|
|
|
xattr_comment = "user.xdg.comment"
|
|
|
xattr_author = "user.xdg.comment.author"
|
|
|
xattr_date = "user.xdg.comment.date"
|
|
@@ -72,11 +75,18 @@ YEAR = 3600*25*365
|
|
|
|
|
|
## globals
|
|
|
mode_names = {"db":"<Database mode> ","xattr":"<Xattr mode>"}
|
|
|
-modes = ("db","xattr")
|
|
|
-mode = "db"
|
|
|
+modes = ("db","xattr")
|
|
|
+mode = "db"
|
|
|
|
|
|
global mainWindow, dbName
|
|
|
|
|
|
+verbose = None
|
|
|
+def print_v(*a):
|
|
|
+ if verbose:
|
|
|
+ print(*a)
|
|
|
+
|
|
|
+############# the DnDataBase, UiHelper and FileObj code is shared with other dirnotes programs
|
|
|
+
|
|
|
DEFAULT_CONFIG_FILE = "~/.dirnotes.conf"
|
|
|
|
|
|
# config
|
|
@@ -89,12 +99,28 @@ DEFAULT_CONFIG = {"xattr_tag":"user.xdg.comment",
|
|
|
"options for start_mode":("db","xattr")
|
|
|
}
|
|
|
|
|
|
-verbose = None
|
|
|
-def print_v(*a):
|
|
|
- if verbose:
|
|
|
- print(*a)
|
|
|
-
|
|
|
-class dnDataBase:
|
|
|
+class ConfigLoader: # singleton
|
|
|
+ def __init__(self, configFile):
|
|
|
+ configFile = os.path.expanduser(configFile)
|
|
|
+ try:
|
|
|
+ with open(configFile,"r") as f:
|
|
|
+ config = json.load(f)
|
|
|
+ except json.JSONDecodeError:
|
|
|
+ errorBox(f"problem reading config file {configFile}; check the JSON syntax")
|
|
|
+ config = DEFAULT_CONFIG
|
|
|
+ except FileNotFoundError:
|
|
|
+ errorBox(f"config file {configFile} not found; using the default settings")
|
|
|
+ config = DEFAULT_CONFIG
|
|
|
+ try:
|
|
|
+ with open(configFile,"w") as f:
|
|
|
+ json.dump(config,f,indent=4)
|
|
|
+ except:
|
|
|
+ errorBox(f"problem creating the config file {configFile}")
|
|
|
+ self.dbName = os.path.expanduser(config["database"])
|
|
|
+ self.mode = config["start_mode"] # can get over-ruled by the command line options
|
|
|
+ self.xattr_comment = config["xattr_tag"]
|
|
|
+
|
|
|
+class DnDataBase:
|
|
|
''' the database is flat
|
|
|
fileName: fully qualified name
|
|
|
st_mtime: a float
|
|
@@ -103,26 +129,37 @@ class dnDataBase:
|
|
|
comment_time: a float, the time of the comment save
|
|
|
author: the username that created the comment
|
|
|
|
|
|
- the database is associated with a user, in the $HOME dir
|
|
|
+ this object: 1) finds or creates the database
|
|
|
+ 2) determine if it's readonly
|
|
|
+
|
|
|
+ TODO: the database is usually associated with a user, in $XDG_DATA_HOME (~/.local/share/)
|
|
|
+ TODO: if the database is not found, create it in $XDG_DATA_DIRS (/usr/local/share/)
|
|
|
+ make it 0666 permissions (rw-rw-rw-)
|
|
|
'''
|
|
|
def __init__(self,dbFile):
|
|
|
'''try to open the database; if not found, create it'''
|
|
|
try:
|
|
|
self.db = sqlite3.connect(dbFile)
|
|
|
except sqlite3.OperationalError:
|
|
|
- print(f"Database {dbFile} not found")
|
|
|
+ logging.error(f"Database {dbFile} not found")
|
|
|
raise
|
|
|
+
|
|
|
+ # create new database if it doesn't exist
|
|
|
try:
|
|
|
self.db.execute("select * from dirnotes")
|
|
|
except sqlite3.OperationalError:
|
|
|
- print_v("Table %s created" % ("dirnotes"))
|
|
|
+ print_v(f"Table dirnotes created")
|
|
|
self.db.execute("create table dirnotes (name TEXT, date DATETIME, size INTEGER, comment TEXT, comment_date DATETIME, author TEXT)")
|
|
|
- self.db_cursor.execute("create index dirnotes_i on dirnotes(name)")
|
|
|
- # getData is only used by the restore-from-database.......consider deleting it
|
|
|
- def getData(self, fileName):
|
|
|
- c = self.db.execute("select * from dirnotes where name=? and comment<>'' order by comment_date desc",(os.path.abspath(fileName),))
|
|
|
- return c.fetchone()
|
|
|
+ self.db.execute("create index dirnotes_i on dirnotes(name)")
|
|
|
+ # at this point, if a shared database is required, somebody needs to set perms to 0o666
|
|
|
+
|
|
|
+ self.writable = True
|
|
|
+ try:
|
|
|
+ self.db.execute("pragma user_verson=0")
|
|
|
+ except sqlite3.OperationalError:
|
|
|
+ self.writable = False
|
|
|
|
|
|
+class UiHelper:
|
|
|
@staticmethod
|
|
|
def epochToDb(epoch):
|
|
|
return time.strftime(DATE_FORMAT,time.localtime(epoch))
|
|
@@ -134,147 +171,194 @@ class dnDataBase:
|
|
|
now = time.time()
|
|
|
diff = now - longDate
|
|
|
if diff > YEAR:
|
|
|
- fmt = "%b %e %Y"
|
|
|
+ fmt = "%b %e %Y"
|
|
|
else:
|
|
|
- fmt = "%b %d %H:%M"
|
|
|
+ fmt = "%b %e %H:%M"
|
|
|
return time.strftime(fmt, time.localtime(longDate))
|
|
|
-
|
|
|
+ @staticmethod
|
|
|
+ def getShortSize(fo):
|
|
|
+ if fo.isDir():
|
|
|
+ return " <DIR> "
|
|
|
+ elif fo.isLink():
|
|
|
+ return " <LINK>"
|
|
|
+ size = fo.getSize()
|
|
|
+ log = int((math.log10(size+1)-2)/3)
|
|
|
+ s = " KMGTE"[log]
|
|
|
+ base = int(size/math.pow(10,log*3))
|
|
|
+ return f"{base}{s}".strip().rjust(7)
|
|
|
+
|
|
|
|
|
|
## one for each file
|
|
|
## and a special one for ".." parent directory
|
|
|
-class FileObj():
|
|
|
- FILE_IS_DIR = -1
|
|
|
- FILE_IS_LINK = -2
|
|
|
- FILE_IS_SOCKET = -3
|
|
|
- def __init__(self, fileName):
|
|
|
- self.fileName = os.path.abspath(fileName) # full path; dirs end WITHOUT a terminal /
|
|
|
- self.displayName = os.path.split(fileName)[1] # base name; dirs end with a /
|
|
|
- s = os.lstat(self.fileName)
|
|
|
- self.date = s.st_mtime
|
|
|
- if stat.S_ISDIR(s.st_mode):
|
|
|
- self.size = FileObj.FILE_IS_DIR
|
|
|
+class FileObj:
|
|
|
+ """ The FileObj knows about both kinds of comments. """
|
|
|
+ def __init__(self, fileName, db):
|
|
|
+ self.fileName = os.path.abspath(fileName) # full path; dirs end WITHOUT a terminal /
|
|
|
+ self.stat = os.lstat(self.fileName)
|
|
|
+ self.displayName = os.path.split(fileName)[1] # base name; dirs end with a /
|
|
|
+ if self.isDir():
|
|
|
if not self.displayName.endswith('/'):
|
|
|
self.displayName += '/'
|
|
|
- elif stat.S_ISLNK(s.st_mode):
|
|
|
- self.size = FileObj.FILE_IS_LINK
|
|
|
- elif stat.S_ISSOCK(s.st_mode):
|
|
|
- self.size = FileObj.FILE_IS_SOCKET
|
|
|
- else:
|
|
|
- self.size = s.st_size
|
|
|
- self.xattrComment = ''
|
|
|
- self.xattrAuthor = None
|
|
|
- self.xattrDate = None
|
|
|
- self.dbComment = ''
|
|
|
- self.dbAuthor = None
|
|
|
- self.dbDate = None
|
|
|
- self.commentsDiffer = False
|
|
|
- try:
|
|
|
- self.xattrComment = os.getxattr(fileName, xattr_comment, follow_symlinks=False).decode()
|
|
|
- self.xattrAuthor = os.getxattr(fileName, xattr_author, follow_symlinks=False).decode()
|
|
|
- self.xattrDate = os.getxattr(fileName, xattr_date, follow_symlinks=False).decode()
|
|
|
- self.commentsDiffer = True if self.xattrComment == self.dbComment else False
|
|
|
- except: # no xattr comment
|
|
|
- pass
|
|
|
+ self.date = self.stat.st_mtime
|
|
|
+ self.size = self.stat.st_size
|
|
|
+ self.db = db
|
|
|
|
|
|
def getName(self):
|
|
|
+ """ returns the absolute pathname """
|
|
|
return self.fileName
|
|
|
def getDisplayName(self):
|
|
|
+ """ returns just this basename of the file; dirs end in / """
|
|
|
return self.displayName
|
|
|
|
|
|
- # with an already open database cursor
|
|
|
- def loadDbComment(self,db):
|
|
|
- c = db.execute("select comment,author,comment_date from dirnotes where name=? and comment<>'' order by comment_date desc",(self.fileName,))
|
|
|
- a = c.fetchone()
|
|
|
- if a:
|
|
|
- self.dbComment, self.dbAuthor, self.dbDate = a
|
|
|
- self.commentsDiffer = True if self.xattrComment == self.dbComment else False
|
|
|
-
|
|
|
+ def getDbData(self):
|
|
|
+ """ returns (comment, author, comment_date) """
|
|
|
+ if not hasattr(self,'dbCommentAuthorDate'):
|
|
|
+ cad = self.db.execute("select comment, author, comment_date from dirnotes where name=? order by comment_date desc",(self.fileName,)).fetchone()
|
|
|
+ self.dbCommentAuthorDate = cad if cad else (None, None, None)
|
|
|
+ return self.dbCommentAuthorDate
|
|
|
def getDbComment(self):
|
|
|
- return self.dbComment
|
|
|
- def getDbAuthor(self):
|
|
|
- return self.dbAuthor
|
|
|
- def getDbDate(self):
|
|
|
- return self.dbDate
|
|
|
- def setDbComment(self,db,newComment):
|
|
|
+ return self.getDbData()[0]
|
|
|
+
|
|
|
+ def getXattrData(self):
|
|
|
+ """ returns (comment, author, comment_date) """
|
|
|
+ if not hasattr(self,'xattrCommentAuthorDate'):
|
|
|
+ c = a = d = None
|
|
|
+ try:
|
|
|
+ c = os.getxattr(self.fileName, xattr_comment, follow_symlinks=False).decode()
|
|
|
+ a = os.getxattr(self.fileName, xattr_author, follow_symlinks=False).decode()
|
|
|
+ d = os.getxattr(self.fileName, xattr_date, follow_symlinks=False).decode()
|
|
|
+ except: # no xattr comment
|
|
|
+ pass
|
|
|
+ self.xattrCommentAuthorDate = c,a,d
|
|
|
+ return self.xattrCommentAuthorDate
|
|
|
+ def getXattrComment(self):
|
|
|
+ return self.getXattrData()[0]
|
|
|
+
|
|
|
+ def setDbComment(self,newComment):
|
|
|
+ # how are we going to hook this?
|
|
|
+ #if not self.db.writable:
|
|
|
+ # errorBox("The database is readonly; you cannot add or edit comments")
|
|
|
+ # return
|
|
|
s = os.lstat(self.fileName)
|
|
|
try:
|
|
|
- # TODO: copy from /g/test_file to /home/patb/project/dirnotes/r fails on database.commit()
|
|
|
- print_v(f"setDbComment db {db}, file: {self.fileName}")
|
|
|
- print_v("insert into dirnotes (name,date,size,comment,comment_date,author) values (?,datetime(?,'unixepoch','localtime'),?,?,datetime(?,'unixepoch','localtime'),?)",
|
|
|
- (self.fileName, s.st_mtime, s.st_size,
|
|
|
- str(newComment), time.time(), getpass.getuser()))
|
|
|
- db.execute("insert into dirnotes (name,date,size,comment,comment_date,author) values (?,datetime(?,'unixepoch','localtime'),?,?,datetime(?,'unixepoch','localtime'),?)",
|
|
|
+ print_v(f"setDbComment db {self.db}, file: {self.fileName}")
|
|
|
+ self.db.execute("insert into dirnotes (name,date,size,comment,comment_date,author) values (?,datetime(?,'unixepoch','localtime'),?,?,datetime(?,'unixepoch','localtime'),?)",
|
|
|
(self.fileName, s.st_mtime, s.st_size,
|
|
|
str(newComment), time.time(), getpass.getuser()))
|
|
|
- print_v(f"setDbComment, execute done, about to commit()")
|
|
|
- db.commit()
|
|
|
- print_v(f"database write for {self.fileName}")
|
|
|
- self.dbComment = newComment
|
|
|
+ self.db.commit()
|
|
|
+ self.dbCommentAuthorDate = newComment, getpass.getuser(), UiHelper.epochToDb(time.time())
|
|
|
except sqlite3.OperationalError:
|
|
|
print_v("database is locked or unwritable")
|
|
|
errorBox("the database that stores comments is locked or unwritable")
|
|
|
- self.commentsDiffer = True if self.xattrComment == self.dbComment else False
|
|
|
|
|
|
- def getXattrComment(self):
|
|
|
- return self.xattrComment
|
|
|
- def getXattrAuthor(self):
|
|
|
- return self.xattrAuthor
|
|
|
- def getXattrDate(self):
|
|
|
- print_v(f"someone accessed date on {self.fileName} {self.xattrDate}")
|
|
|
- return self.xattrDate
|
|
|
def setXattrComment(self,newComment):
|
|
|
print_v(f"set comment {newComment} on file {self.fileName}")
|
|
|
try:
|
|
|
os.setxattr(self.fileName,xattr_comment,bytes(newComment,'utf8'),follow_symlinks=False)
|
|
|
os.setxattr(self.fileName,xattr_author,bytes(getpass.getuser(),'utf8'),follow_symlinks=False)
|
|
|
os.setxattr(self.fileName,xattr_date,bytes(time.strftime(DATE_FORMAT),'utf8'),follow_symlinks=False)
|
|
|
- self.xattrAuthor = getpass.getuser()
|
|
|
- self.xattrDate = time.strftime(DATE_FORMAT) # alternatively, re-instantiate this FileObj
|
|
|
- self.xattrComment = newComment
|
|
|
- self.commentsDiffer = True if self.xattrComment == self.dbComment else False
|
|
|
+ self.xattrCommentAuthorDate = newComment, getpass.getuser(), time.strftime(DATE_FORMAT)
|
|
|
return True
|
|
|
# we need to move these cases out to a handler
|
|
|
except Exception as e:
|
|
|
- if self.size == FileObj.FILE_IS_LINK:
|
|
|
+ if self.isLink():
|
|
|
errorBox("Linux does not allow xattr comments on symlinks; comment is stored in database")
|
|
|
- elif self.size == FileObj.FILE_IS_SOCKET:
|
|
|
+ elif self.isSock():
|
|
|
errorBox("Linux does not allow comments on sockets; comment is stored in database")
|
|
|
elif os.access(self.fileName, os.W_OK)!=True:
|
|
|
errorBox("you don't appear to have write permissions on this file")
|
|
|
# change the listbox background to yellow
|
|
|
- self.displayBox.notifyUnchanged()
|
|
|
elif "Errno 95" in str(e):
|
|
|
errorBox("is this a VFAT or EXFAT volume? these don't allow comments")
|
|
|
- self.commentsDiffer = True
|
|
|
return False
|
|
|
- self.commentsDiffer = True if self.xattrComment == self.dbComment else False
|
|
|
|
|
|
- def getComment(self):
|
|
|
- return self.getDbComment() if mode == "db" else self.getXattrComment()
|
|
|
- def getOtherComment(self):
|
|
|
+ def getComment(self,mode):
|
|
|
+ return self.getDbComment() if mode == "db" else self.getXattrComment()
|
|
|
+ def getOtherComment(self,mode):
|
|
|
return self.getDbComment() if mode == "xattr" else self.getXattrComment()
|
|
|
+ def getData(self,mode):
|
|
|
+ """ returns (comment, author, comment_date) """
|
|
|
+ return self.getDbData() if mode == "db" else self.getXattrData()
|
|
|
+ def getOtherData(self,mode):
|
|
|
+ """ returns (comment, author, comment_date) """
|
|
|
+ return self.getDbData() if mode == "xattr" else self.getXattrData()
|
|
|
+
|
|
|
def getDate(self):
|
|
|
return self.date
|
|
|
def getSize(self):
|
|
|
return self.size
|
|
|
def isDir(self):
|
|
|
- return self.size == self.FILE_IS_DIR
|
|
|
+ return stat.S_ISDIR(self.stat.st_mode)
|
|
|
def isLink(self):
|
|
|
- return self.size == self.FILE_IS_LINK
|
|
|
+ return stat.S_ISLNK(self.stat.st_mode)
|
|
|
+ def isSock(self):
|
|
|
+ return stat.S_ISSOCK(self.stat.st_mode)
|
|
|
+
|
|
|
+ def copyFile(self, destDir):
|
|
|
+ # NOTE: this method copies the xattr (comment + old author + old date)
|
|
|
+ # but creates new db (comment + this author + new date)
|
|
|
+ dest = os.path.join(destDir,self.displayName)
|
|
|
+ try:
|
|
|
+ print_v("try copy from",self.fileName,"to",dest)
|
|
|
+ shutil.copy2(self.fileName, dest)
|
|
|
+ except:
|
|
|
+ errorBox(f"file copy to <{dest}> failed; check permissions")
|
|
|
+ return
|
|
|
+ f = FileObj(dest, self.db)
|
|
|
+ f.setDbComment(self.getDbComment())
|
|
|
+ def moveFile(self, destDir):
|
|
|
+ # NOTE: this method moves the xattr (comment + old author + old date)
|
|
|
+ # but creates new db (comment + this author + new date)
|
|
|
+ src = self.fileName
|
|
|
+ dest = os.path.join(destDir, self.displayName)
|
|
|
+ # move preserves dates & chmod/chown & xattr
|
|
|
+ print_v(f"move from {self.fileName} to {destDir}")
|
|
|
+ try:
|
|
|
+ shutil.move(src, dest)
|
|
|
+ except:
|
|
|
+ ErrorBox(f"file move to <{dest}> failed; check permissions")
|
|
|
+ return
|
|
|
+ # and copy the database record
|
|
|
+ f = FileObj(dest,self.db)
|
|
|
+ f.setDbComment(self.getDbComment())
|
|
|
|
|
|
class HelpWidget(QDialog):
|
|
|
def __init__(self, parent):
|
|
|
super(QDialog, self).__init__(parent)
|
|
|
- self.layout = QVBoxLayout(self)
|
|
|
- self.tb = QLabel(self)
|
|
|
- self.tb.setWordWrap(True)
|
|
|
- self.tb.setText(helpMsg)
|
|
|
- self.tb.setFixedWidth(500)
|
|
|
- self.pb = QPushButton('OK',self)
|
|
|
- self.pb.setFixedWidth(200)
|
|
|
- self.layout.addWidget(self.tb)
|
|
|
- self.layout.addWidget(self.pb)
|
|
|
- self.pb.clicked.connect(self.close)
|
|
|
+ layout = QVBoxLayout(self)
|
|
|
+
|
|
|
+ tb = QLabel(self)
|
|
|
+ tb.setWordWrap(True)
|
|
|
+ tb.setText(helpMsg)
|
|
|
+ tb.setFixedWidth(500)
|
|
|
+ pb = QPushButton('OK',self)
|
|
|
+ pb.setFixedWidth(200)
|
|
|
+ kb = QPushButton('Keyboard Help',self)
|
|
|
+
|
|
|
+ layout.addWidget(tb)
|
|
|
+ lowerBox = QHBoxLayout(self)
|
|
|
+ lowerBox.addWidget(pb)
|
|
|
+ lowerBox.addWidget(kb)
|
|
|
+ layout.addLayout(lowerBox)
|
|
|
+
|
|
|
+ pb.clicked.connect(self.close)
|
|
|
+ kb.clicked.connect(self.showKeyboardHelp)
|
|
|
+ self.show()
|
|
|
+ def showKeyboardHelp(self):
|
|
|
+ KeyboardHelpWidget(self)
|
|
|
+
|
|
|
+class KeyboardHelpWidget(QDialog):
|
|
|
+ def __init__(self, parent):
|
|
|
+ super(QDialog, self).__init__(parent)
|
|
|
+ layout = QVBoxLayout(self)
|
|
|
+ tb = QLabel(self)
|
|
|
+ tb.setWordWrap(True)
|
|
|
+ tb.setText(keyboardHelpMsg)
|
|
|
+ tb.setFixedWidth(500)
|
|
|
+ pb = QPushButton('OK',self)
|
|
|
+ layout.addWidget(tb)
|
|
|
+ layout.addWidget(pb)
|
|
|
+ pb.clicked.connect(self.close)
|
|
|
self.show()
|
|
|
|
|
|
class errorBox(QDialog):
|
|
@@ -292,6 +376,30 @@ class errorBox(QDialog):
|
|
|
self.pb.clicked.connect(self.close)
|
|
|
self.show()
|
|
|
|
|
|
+keyboardHelpMsg = """
|
|
|
+<h2>Keyboard Shortcuts</h2>
|
|
|
+<p>
|
|
|
+<table width=100%>
|
|
|
+<tr><td><i>Arrows</i></td><td>normal movement through the table</td></tr>
|
|
|
+<tr><td>Ctrl+N</td><td>sort the listing by filename</td></tr>
|
|
|
+<tr><td>Ctrl+D</td><td>sort the listing by date</td></tr>
|
|
|
+<tr><td>Ctrl+S</td><td>sort the listing by size</td></tr>
|
|
|
+<tr><td>Ctrl+T</td><td>sort the listing by comment</td></tr>
|
|
|
+<tr><td>Ctrl+M</td><td>toggle between <i>database</i> and <i>xattr</i> views</td></tr>
|
|
|
+<tr><td>Alt+C </td><td>copy the file <i>and its comments</i></td></tr>
|
|
|
+<tr><td>Alt+M </td><td>copy the file <i>and its comments</i></td></tr>
|
|
|
+
|
|
|
+<tr><td>1st column: <i>any letter</i></td><td>jump to file beginning with that letter</td></tr>
|
|
|
+<tr><td>1st column: <Enter> </td><td>change directory</td></tr>
|
|
|
+<tr><td>4th column: <i>any letter</i></td><td>create a comment; replace any existing comment</td></tr>
|
|
|
+<tr><td>4th column: <Enter> </td><td>open an existing comment for edit</td></tr>
|
|
|
+
|
|
|
+<tr><td>Ctrl+Q</td><td>quit the app</td></tr>
|
|
|
+</table>
|
|
|
+<p>
|
|
|
+NOTE: In edit mode, Ctrl+C, Ctrl+V and Ctrl+P work for cut, copy and paste.
|
|
|
+"""
|
|
|
+
|
|
|
icon = ["32 32 6 1", # the QPixmap constructor allows for str[]
|
|
|
" c None",
|
|
|
". c #666666",
|
|
@@ -336,35 +444,25 @@ icon = ["32 32 6 1", # the QPixmap constructor allows for str[]
|
|
|
# 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
|
|
|
+# we could use the Qt.DisplayRole/Qt.EditRole for display, and Qt.UserRole for sorting
|
|
|
# in Qt5, data() binding is more awkward, so do it here
|
|
|
class SortableTableWidgetItem(QTableWidgetItem):
|
|
|
def __init__(self, text, sortValue, file_object):
|
|
|
- QTableWidgetItem.__init__(self, text, QTableWidgetItem.UserType)
|
|
|
+ QTableWidgetItem.__init__(self, text, QTableWidgetItem.UserType + 1)
|
|
|
self.sortValue = sortValue
|
|
|
self.file_object = file_object
|
|
|
def __lt__(self, other):
|
|
|
return self.sortValue < other.sortValue
|
|
|
|
|
|
-
|
|
|
class DirNotes(QMainWindow):
|
|
|
''' the main window of the app
|
|
|
'''
|
|
|
- def __init__(self, argFilename, db, start_mode, parent=None):
|
|
|
+ def __init__(self, argFilename, db, parent=None):
|
|
|
super(DirNotes,self).__init__(parent)
|
|
|
self.db = db
|
|
|
self.refilling = False
|
|
|
self.parent = parent
|
|
|
|
|
|
- win = QWidget()
|
|
|
- self.setCentralWidget(win)
|
|
|
-
|
|
|
- lb = QTableWidget()
|
|
|
- self.lb = lb
|
|
|
- lb.setColumnCount(4)
|
|
|
- lb.horizontalHeader().setSectionResizeMode( 3, QHeaderView.Stretch );
|
|
|
- lb.verticalHeader().setDefaultSectionSize(20); # thinner rows
|
|
|
- lb.verticalHeader().setVisible(False)
|
|
|
-
|
|
|
longPathName = os.path.abspath(argFilename)
|
|
|
print_v("longpathname is {}".format(longPathName))
|
|
|
if os.path.isdir(longPathName):
|
|
@@ -373,21 +471,49 @@ class DirNotes(QMainWindow):
|
|
|
else:
|
|
|
self.curPath, filename = os.path.split(longPathName)
|
|
|
print_v("working on <"+self.curPath+"> and <"+filename+">")
|
|
|
+
|
|
|
+ win = QWidget()
|
|
|
+ self.setCentralWidget(win)
|
|
|
|
|
|
- layout = QVBoxLayout()
|
|
|
+ mb = self.menuBar()
|
|
|
+ mf = mb.addMenu('&File')
|
|
|
+ mf.addAction("Sort by name", self.sbn, "Ctrl+N")
|
|
|
+ mf.addAction("Sort by date", self.sbd, "Ctrl+D")
|
|
|
+ mf.addAction("Sort by size", self.sbs, "Ctrl+Z")
|
|
|
+ mf.addAction("Sort by comment", self.sbc, "Ctrl+T")
|
|
|
+ mf.addSeparator()
|
|
|
+ mf.addAction("Change mode", self.switchMode, "Ctrl+M")
|
|
|
+ mf.addAction("Copy file", self.copyFile, "Alt+C")
|
|
|
+ mf.addAction("Move file", self.moveFile, "Alt+M")
|
|
|
+ mf.addSeparator()
|
|
|
+ mf.addAction("Quit", self.close, QKeySequence.Quit)
|
|
|
+ mf.addAction("About", self.about, QKeySequence.HelpContents)
|
|
|
|
|
|
- copyIcon = QIcon.fromTheme('drive-harddisk-symbolic')
|
|
|
- changeIcon = QIcon.fromTheme('emblem-synchronizing-symbolic')
|
|
|
+ self.setWindowTitle("==DirNotes== Dir: "+self.curPath)
|
|
|
+ self.setMinimumSize(600,700)
|
|
|
+ self.setWindowIcon(QIcon(QPixmap(icon)))
|
|
|
|
|
|
- topLayout = QHBoxLayout()
|
|
|
+ lb = QTableWidget()
|
|
|
+ self.lb = lb
|
|
|
+ lb.setColumnCount(4)
|
|
|
+ lb.horizontalHeader().setSectionResizeMode( 3, QHeaderView.Stretch );
|
|
|
+ lb.verticalHeader().setDefaultSectionSize(20); # thinner rows
|
|
|
+ lb.verticalHeader().setVisible(False)
|
|
|
+
|
|
|
self.modeShow = QLabel(win)
|
|
|
+ copyIcon = QIcon.fromTheme('edit-copy')
|
|
|
+ changeIcon = QIcon.fromTheme('emblem-synchronizing')
|
|
|
+ bmode = QPushButton(changeIcon, "change mode (ctrl+m)",win)
|
|
|
+ cf = QPushButton(copyIcon, "copy file",win)
|
|
|
+ self.thisDirLabel = QLabel(win)
|
|
|
+
|
|
|
+ layout = QVBoxLayout()
|
|
|
+ topLayout = QHBoxLayout()
|
|
|
topLayout.addWidget(self.modeShow)
|
|
|
- bmode = QPushButton(changeIcon, "change mode",win)
|
|
|
topLayout.addWidget(bmode)
|
|
|
- cf = QPushButton(copyIcon, "copy file",win)
|
|
|
topLayout.addWidget(cf)
|
|
|
layout.addLayout(topLayout)
|
|
|
-
|
|
|
+ layout.addWidget(self.thisDirLabel)
|
|
|
layout.addWidget(lb)
|
|
|
win.setLayout(layout)
|
|
|
|
|
@@ -402,49 +528,34 @@ class DirNotes(QMainWindow):
|
|
|
lb.setHorizontalHeaderItem(3,QTableWidgetItem("Comment"))
|
|
|
lb.setSortingEnabled(True)
|
|
|
|
|
|
+ self.sameBrush = QBrush(QColor(255,255,255))
|
|
|
+ self.differBrush = QBrush(QColor(255,255,160))
|
|
|
+
|
|
|
self.refill()
|
|
|
lb.resizeColumnsToContents()
|
|
|
|
|
|
- if len(filename)>0:
|
|
|
+ if filename:
|
|
|
for i in range(lb.rowCount()):
|
|
|
- if filename == lb.item(i,0).data(32).getDisplayName():
|
|
|
- lb.setCurrentCell(i,3)
|
|
|
+ if filename == lb.item(i,0).file_object.getDisplayName():
|
|
|
+ lb.setCurrentCell(i,0)
|
|
|
break
|
|
|
-
|
|
|
- mb = self.menuBar()
|
|
|
- mf = mb.addMenu('&File')
|
|
|
- mf.addAction("Sort by name", self.sbn, "Ctrl+N")
|
|
|
- mf.addAction("Sort by date", self.sbd, "Ctrl+D")
|
|
|
- mf.addAction("Sort by size", self.sbs, "Ctrl+Z")
|
|
|
- mf.addAction("Sort by comment", self.sbc, "Ctrl+.")
|
|
|
- mf.addAction("Restore comment from database", self.restore_from_database, "Ctrl+R")
|
|
|
- mf.addSeparator()
|
|
|
- mf.addAction("Quit", self.close, QKeySequence.Quit)
|
|
|
- mf.addAction("About", self.about, QKeySequence.HelpContents)
|
|
|
-
|
|
|
- self.setWindowTitle("DirNotes Alt-F for menu Dir: "+self.curPath)
|
|
|
- self.setMinimumSize(600,700)
|
|
|
- self.setWindowIcon(QIcon(QPixmap(icon)))
|
|
|
lb.setFocus()
|
|
|
|
|
|
- def closeEvent(self,e):
|
|
|
- print("closing")
|
|
|
def sbd(self):
|
|
|
- print("sort by date")
|
|
|
+ print_v("sort by date")
|
|
|
self.lb.sortItems(1,Qt.DescendingOrder)
|
|
|
def sbs(self):
|
|
|
- print("sort by size")
|
|
|
+ print_v("sort by size")
|
|
|
self.lb.sortItems(2)
|
|
|
def sbn(self):
|
|
|
- print("sort by name")
|
|
|
+ print_v("sort by name")
|
|
|
self.lb.sortItems(0)
|
|
|
+ def sbc(self):
|
|
|
+ print_v("sort by comment")
|
|
|
+ self.lb.sortItems(3)
|
|
|
def about(self):
|
|
|
HelpWidget(self)
|
|
|
- def sbc(self):
|
|
|
- print("sort by comment")
|
|
|
- self.lb.sortItems(3,Qt.DescendingOrder)
|
|
|
- def newDir(self):
|
|
|
- print("change dir to "+self.dirLeft.currentPath())
|
|
|
+
|
|
|
def double(self,row,col):
|
|
|
print_v("double click {} {}".format(row, col))
|
|
|
fo = self.lb.item(row,0).file_object
|
|
@@ -452,38 +563,57 @@ class DirNotes(QMainWindow):
|
|
|
print_v("double click on {}".format(fo.getName()))
|
|
|
self.curPath = fo.getName()
|
|
|
self.refill()
|
|
|
- def copyFile(self):
|
|
|
+ def keyPressEvent(self,e):
|
|
|
+ if e.key() in (Qt.Key_Return, Qt.Key_Enter):
|
|
|
+ col = self.lb.currentColumn()
|
|
|
+ fo = self.lb.item(self.lb.currentRow(),0).file_object
|
|
|
+ if col==0 and fo and fo.isDir():
|
|
|
+ self.curPath = fo.getName()
|
|
|
+ self.refill()
|
|
|
+ return
|
|
|
+ if col==3:
|
|
|
+ self.lb.editItem(self.lb.currentItem())
|
|
|
+ return
|
|
|
+ #self.lb.superKeyEvent(e)
|
|
|
+ super().keyPressEvent(e)
|
|
|
+
|
|
|
+ def copyMoveFile(self, doCopy, pickerTitle):
|
|
|
# get current selection
|
|
|
r, c = self.lb.currentRow(), self.lb.currentColumn()
|
|
|
fo = self.lb.item(r,c).file_object
|
|
|
- if not fo.isDir() and not fo.isLink(): # TODO: add check for socket
|
|
|
- print_v(f"copy file {fo.getName()}")
|
|
|
+ if not fo.isDir() and not fo.isLink() and not fo.isSock():
|
|
|
+ print_v(f"{'copy' if doCopy=='copy' else 'move'} file {fo.getName()}")
|
|
|
# open the dir.picker
|
|
|
- r = QFileDialog.getExistingDirectory(self.parent, "Select destination for FileCopy")
|
|
|
- print_v(f"copy to {r}")
|
|
|
- if r:
|
|
|
- dest = os.path.join(r,fo.getDisplayName())
|
|
|
- try:
|
|
|
- shutil.copy2(fo.getName(), dest) # copy2 preserves the xattr
|
|
|
- f = FileObj(dest) # can't make the FileObj until it exists
|
|
|
- f.setDbComment(self.db,fo.getDbComment())
|
|
|
- except:
|
|
|
- errorBox(f"file copy to <{dest}> failed; check permissions")
|
|
|
- pass
|
|
|
-
|
|
|
+ d = QFileDialog.getExistingDirectory(self.parent, pickerTitle)
|
|
|
+ if d:
|
|
|
+ print_v(f"senf file to {d}")
|
|
|
+ fo.copyFile(d) if doCopy=='copy' else fo.moveFile(d)
|
|
|
+ def copyFile(self):
|
|
|
+ self.copyMoveFile('copy',"Select destination for FileCopy")
|
|
|
+ def moveFile(self):
|
|
|
+ self.copyMoveFile('move',"Select destination for FileMove")
|
|
|
+ self.refill()
|
|
|
+
|
|
|
def refill(self):
|
|
|
self.refilling = True
|
|
|
self.lb.sortingEnabled = False
|
|
|
+ self.directory = FileObj(self.curPath,self.db)
|
|
|
|
|
|
- (self.modeShow.setText("View and edit file comments stored in extended attributes\n(xattr: user.xdg.comment)")
|
|
|
+ self.thisDirLabel.setText(f'<table width=100%><tr><th><b>{self.directory.getDisplayName()}</b></th><th style"text-align:right;">{self.directory.getComment(mode)}</th></tr></table>')
|
|
|
+ (self.modeShow.setText("<i>Showing comments stored in extended attributes</i><br>(xattr: user.xdg.comment)")
|
|
|
if mode=="xattr" else
|
|
|
- self.modeShow.setText("View and edit file comments stored in the database \n(~/.dirnotes.db)"))
|
|
|
+ self.modeShow.setText("<i>Showing comments from the database</i><br>(~/.dirnotes.db)"))
|
|
|
self.lb.clearContents()
|
|
|
- small_font = QFont("",8)
|
|
|
- dirIcon = QIcon.fromTheme('folder')
|
|
|
+ dirIcon = QIcon.fromTheme('folder')
|
|
|
fileIcon = QIcon.fromTheme('text-x-generic')
|
|
|
linkIcon = QIcon.fromTheme('emblem-symbolic-link')
|
|
|
- current, dirs, files = next(os.walk(self.curPath,followlinks=True))
|
|
|
+ sockIcon = QIcon.fromTheme('emblem-shared')
|
|
|
+
|
|
|
+ try:
|
|
|
+ current, dirs, files = next(os.walk(self.curPath,followlinks=True))
|
|
|
+ except:
|
|
|
+ print(f"{self.curPath} is not a valid directory")
|
|
|
+ sys.exit(1)
|
|
|
dirs.sort()
|
|
|
files.sort()
|
|
|
|
|
@@ -492,57 +622,57 @@ class DirNotes(QMainWindow):
|
|
|
d = dirs + files
|
|
|
self.lb.setRowCount(len(d))
|
|
|
|
|
|
- #~ self.files = {}
|
|
|
- self.files = []
|
|
|
- # this is a list of all the file
|
|
|
#~ print("insert {} items into cleared table {}".format(len(d),current))
|
|
|
for i,name in enumerate(d):
|
|
|
- this_file = FileObj(os.path.join(current,name))
|
|
|
- this_file.loadDbComment(self.db)
|
|
|
- print_v("FileObj created as {} and the db-comment is <{}>".format(this_file.displayName, this_file.dbComment))
|
|
|
+ this_file = FileObj(os.path.join(current,name),self.db)
|
|
|
+ print_v("FileObj created as {} and the db-comment is <{}>".format(this_file.displayName, this_file.getDbComment))
|
|
|
#~ 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]
|
|
|
display_name = this_file.getDisplayName()
|
|
|
- if this_file.getSize() == FileObj.FILE_IS_DIR:
|
|
|
+ if this_file.isDir():
|
|
|
item = SortableTableWidgetItem(display_name,' '+display_name, this_file) # directories sort first
|
|
|
else:
|
|
|
item = SortableTableWidgetItem(display_name,display_name, this_file)
|
|
|
- item.setData(32,this_file) # keep a hidden copy of the file object
|
|
|
item.setToolTip(this_file.getName())
|
|
|
+ item.setFlags(Qt.ItemIsEnabled)
|
|
|
self.lb.setItem(i,0,item)
|
|
|
- #lb.itemAt(i,0).setFlags(Qt.ItemIsEnabled) #NoItemFlags)
|
|
|
|
|
|
# get the comment from database & xattrs, either can fail
|
|
|
- comment = this_file.getComment()
|
|
|
- other_comment = this_file.getOtherComment()
|
|
|
- ci = SortableTableWidgetItem(comment,'',this_file)
|
|
|
- ci.setToolTip(f"comment: {comment}\ncomment date: {this_file.getDbDate()}\nauthor: {this_file.getDbAuthor()}")
|
|
|
+ comment, auth, cdate = this_file.getData(mode)
|
|
|
+ other_comment = this_file.getOtherComment(mode)
|
|
|
+ ci = SortableTableWidgetItem(comment,comment or '~',this_file)
|
|
|
+ ci.setToolTip(f"comment: {comment}\ncomment date: {cdate}\nauthor: {auth}")
|
|
|
if other_comment != comment:
|
|
|
- ci.setBackground(QBrush(QColor(255,255,160)))
|
|
|
+ ci.setBackground(self.differBrush)
|
|
|
print_v("got differing comments <{}> and <{}>".format(comment, other_comment))
|
|
|
+ ci.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEditable | Qt.ItemIsEnabled)
|
|
|
self.lb.setItem(i,3,ci)
|
|
|
|
|
|
dt = this_file.getDate()
|
|
|
- da = SortableTableWidgetItem(dnDataBase.getShortDate(dt),dt,this_file)
|
|
|
+ da = SortableTableWidgetItem(UiHelper.getShortDate(dt),dt,this_file)
|
|
|
da.setToolTip(time.strftime(DATE_FORMAT,time.localtime(dt)))
|
|
|
+ da.setFlags(Qt.ItemIsEnabled)
|
|
|
self.lb.setItem(i,1,da)
|
|
|
|
|
|
si = this_file.getSize()
|
|
|
if this_file.isDir():
|
|
|
- sa = SortableTableWidgetItem('',0,this_file)
|
|
|
- item.setIcon(dirIcon)
|
|
|
+ sa = SortableTableWidgetItem(UiHelper.getShortSize(this_file),0,this_file)
|
|
|
+ sa.setIcon(dirIcon)
|
|
|
elif this_file.isLink():
|
|
|
- sa = SortableTableWidgetItem('symlink',-1,this_file)
|
|
|
- item.setIcon(linkIcon)
|
|
|
+ sa = SortableTableWidgetItem(UiHelper.getShortSize(this_file),-1,this_file)
|
|
|
+ sa.setIcon(linkIcon)
|
|
|
dst = os.path.realpath(this_file.getName())
|
|
|
sa.setToolTip(f"symlink: {dst}")
|
|
|
+ elif this_file.isSock():
|
|
|
+ sa = SortableTableWidgetItem(UiHelper.getShortSize(this_file),-1,this_file)
|
|
|
+ sa.setIcon(sockIcon)
|
|
|
else:
|
|
|
- sa = SortableTableWidgetItem(str(si),si,this_file)
|
|
|
- item.setIcon(fileIcon)
|
|
|
+ sa = SortableTableWidgetItem(UiHelper.getShortSize(this_file),si,this_file)
|
|
|
+ sa.setIcon(fileIcon)
|
|
|
sa.setTextAlignment(Qt.AlignRight)
|
|
|
+ sa.setFlags(Qt.ItemIsEnabled)
|
|
|
self.lb.setItem(i,2,sa)
|
|
|
|
|
|
+ self.lb.setCurrentCell(0,0)
|
|
|
self.refilling = False
|
|
|
self.lb.sortingEnabled = True
|
|
|
self.lb.resizeColumnToContents(1)
|
|
@@ -550,41 +680,28 @@ class DirNotes(QMainWindow):
|
|
|
def change(self,x):
|
|
|
if self.refilling:
|
|
|
return
|
|
|
- print_v("debugging " + x.text() + " r:" + str(x.row()) + " c:" + str(x.column()))
|
|
|
- print_v(" selected file: "+self.lb.item(x.row(),0).file_object.getName())
|
|
|
the_file = self.lb.item(x.row(),0).file_object
|
|
|
- print_v(" and the row file is "+the_file.getName())
|
|
|
- the_file.setDbComment(self.db,str(x.text()))
|
|
|
- r = the_file.setXattrComment(str(x.text()))
|
|
|
- if r:
|
|
|
- the_file.setDbComment(self.db,x.text())
|
|
|
+ print_v(f"debugging {x.text()} r:{str(x.row())} c:{str(x.column())}")
|
|
|
+ print_v(f" selected file: {the_file.getName()}")
|
|
|
+ the_file.setDbComment(str(x.text()))
|
|
|
+ the_file.setXattrComment(str(x.text()))
|
|
|
+
|
|
|
+ # set the background (wrap it, because of reentry to .change())
|
|
|
+ self.refilling = True
|
|
|
+ if the_file.getComment(mode) == the_file.getOtherComment(mode):
|
|
|
+ x.setBackground(self.sameBrush)
|
|
|
+ else:
|
|
|
+ x.setBackground(self.differBrush)
|
|
|
+ self.refilling = False
|
|
|
|
|
|
def switchMode(self):
|
|
|
global mode
|
|
|
mode = "xattr" if mode == "db" else "db"
|
|
|
+ row,column = self.lb.currentRow(), self.lb.currentColumn()
|
|
|
self.refill()
|
|
|
+ self.lb.setCurrentCell(row,column)
|
|
|
+ self.lb.setFocus(True)
|
|
|
|
|
|
- # TODO: this may not be needed
|
|
|
- def restore_from_database(self):
|
|
|
- print("restore from database")
|
|
|
- # retrieve the full path name
|
|
|
- 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+"=")
|
|
|
- if len(existing_comment) > 0:
|
|
|
- m = QMessageBox()
|
|
|
- m.setText("This file already has a comment. Overwrite?")
|
|
|
- m.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel);
|
|
|
- if m.exec_() != QMessageBox.Ok:
|
|
|
- return
|
|
|
- fo_row = self.db.getData(fileName)
|
|
|
- if fo_row and len(fo_row)>1:
|
|
|
- comment = fo_row[3]
|
|
|
- print(fileName,fo_row[0],comment)
|
|
|
- the_file = dn.files[self.lb.currentRow()]
|
|
|
- the_file.setComment(comment)
|
|
|
- self.lb.setItem(self.lb.currentRow(),3,QTableWidgetItem(comment))
|
|
|
|
|
|
def parse():
|
|
|
parser = argparse.ArgumentParser(description='dirnotes application')
|
|
@@ -614,33 +731,28 @@ if __name__=="__main__":
|
|
|
print_v(f"using {p.dirname}")
|
|
|
verbose = p.verbose
|
|
|
|
|
|
- config_file = p.config_file if p.config_file else DEFAULT_CONFIG_FILE
|
|
|
- config_file = os.path.expanduser(config_file)
|
|
|
- config = DEFAULT_CONFIG
|
|
|
- try:
|
|
|
- with open(config_file,"r") as f:
|
|
|
- config = json.load(f)
|
|
|
- except json.JSONDecodeError:
|
|
|
- print(f"problem reading config file {config_file}; check the .json syntax")
|
|
|
- except FileNotFoundError:
|
|
|
- print(f"config file {config_file} not found, using the default settings and writing a default")
|
|
|
- try:
|
|
|
- with open(config_file,"w") as f:
|
|
|
- json.dump(config,f,indent=4)
|
|
|
- except:
|
|
|
- print(f"problem creating the file {config_file}")
|
|
|
+ config = ConfigLoader(p.config_file or DEFAULT_CONFIG_FILE)
|
|
|
|
|
|
print_v(f"here is the .json {repr(config)}")
|
|
|
- dbName = os.path.expanduser(config["database"])
|
|
|
- db = dnDataBase(dbName).db
|
|
|
- xattr_comment = config["xattr_tag"]
|
|
|
+ dbName = config.dbName
|
|
|
+ db = DnDataBase(dbName).db
|
|
|
+ xattr_comment = config.xattr_comment
|
|
|
xattr_author = xattr_comment + ".author"
|
|
|
xattr_date = xattr_comment + ".date"
|
|
|
|
|
|
- mode = "xattr" if p.xattr else "db"
|
|
|
+ mode = config.mode
|
|
|
+ if p.xattr:
|
|
|
+ mode = "xattr"
|
|
|
+ if p.db:
|
|
|
+ mode = "db"
|
|
|
|
|
|
a = QApplication([])
|
|
|
- mainWindow = DirNotes(p.dirname,db,config["start_mode"])
|
|
|
+ # TODO: add 'mode' as an argument to contructor; add setMode() as a method
|
|
|
+ mainWindow = DirNotes(p.dirname,db)
|
|
|
+ if p.sort_by_size:
|
|
|
+ mainWindow.sbs()
|
|
|
+ if p.sort_by_date:
|
|
|
+ mainWindow.sbd()
|
|
|
mainWindow.show()
|
|
|
|
|
|
if p.sort_by_date:
|
|
@@ -650,21 +762,6 @@ if __name__=="__main__":
|
|
|
|
|
|
a.exec_()
|
|
|
|
|
|
- #xattr.setxattr(filename,COMMENT_KEY,commentText)
|
|
|
-
|
|
|
-''' files from directories
|
|
|
-use os.isfile()
|
|
|
-os.isdir()
|
|
|
-current, dirs, files = os.walk("path").next()
|
|
|
-possible set folllowLinks=True'''
|
|
|
-
|
|
|
-''' notes from the wdrm project
|
|
|
-table showed
|
|
|
-filename, size, date size, date, desc
|
|
|
-
|
|
|
-at start, fills the list of all the files
|
|
|
-skip the . entry
|
|
|
-'''
|
|
|
|
|
|
''' should we also do user.xdg.tags="TagA,TagB" ?
|
|
|
user.charset
|
|
@@ -674,19 +771,19 @@ user.xdg.language=[RFC3066/ISO639]
|
|
|
user.xdg.publisher
|
|
|
'''
|
|
|
|
|
|
-''' TODO: add cut-copy-paste for comments '''
|
|
|
-
|
|
|
''' TODO: also need a way to display-&-restore comments from the database '''
|
|
|
|
|
|
''' TODO: implement startup -s and -m for size and date '''
|
|
|
|
|
|
-''' TODO: add an icon for the app '''
|
|
|
-
|
|
|
''' TODO: create 'show comment history' popup '''
|
|
|
-
|
|
|
-''' TODO: add dual-pane for file-move, file-copy '''
|
|
|
|
|
|
''' commandline xattr
|
|
|
getfattr -h (don't follow symlink) -d (dump all properties)
|
|
|
'''
|
|
|
-''' if the args line contains a file, jump to it '''
|
|
|
+
|
|
|
+''' CODING NOTES:
|
|
|
+ in FileObj, the long absolute name always ends without a /
|
|
|
+ the short display name ends with a / if it's a directory
|
|
|
+ dates are always in YYYY-MM-DD HH:MM:SS format
|
|
|
+ these can be sorted
|
|
|
+'''
|