|  | @@ -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.7"
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  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
 | 
	
		
			
				|  |  | +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,8 +75,8 @@ 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
 | 
	
		
			
				|  |  |  
 | 
	
	
		
			
				|  | @@ -94,6 +97,7 @@ def print_v(*a):
 | 
	
		
			
				|  |  |    if verbose:
 | 
	
		
			
				|  |  |      print(*a)
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +############# the dnDataBase and FileObj code is shared with other dirnotes programs
 | 
	
		
			
				|  |  |  class dnDataBase:
 | 
	
		
			
				|  |  |    ''' the database is flat
 | 
	
		
			
				|  |  |      fileName: fully qualified name
 | 
	
	
		
			
				|  | @@ -103,25 +107,35 @@ 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
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |    @staticmethod
 | 
	
		
			
				|  |  |    def epochToDb(epoch):
 | 
	
	
		
			
				|  | @@ -136,145 +150,162 @@ class dnDataBase:
 | 
	
		
			
				|  |  |      if diff > YEAR:
 | 
	
		
			
				|  |  |        fmt = "%b %e %Y"
 | 
	
		
			
				|  |  |      else:
 | 
	
		
			
				|  |  | -      fmt = "%b %d %H:%M"
 | 
	
		
			
				|  |  | +      fmt = "%b %e %H:%M"
 | 
	
		
			
				|  |  |      return time.strftime(fmt, time.localtime(longDate))
 | 
	
		
			
				|  |  |      
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  ## 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
 | 
	
		
			
				|  |  | +  """  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):
 | 
	
		
			
				|  |  |      return self.fileName
 | 
	
		
			
				|  |  |    def getDisplayName(self):
 | 
	
		
			
				|  |  |      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
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | +  # new methods; no caching
 | 
	
		
			
				|  |  | +  def getDbData(self):
 | 
	
		
			
				|  |  | +    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):
 | 
	
		
			
				|  |  | +    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(), dnDataBase.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()
 | 
	
		
			
				|  |  | +    return self.getDbComment() if mode == "db"    else self.getXattrComment()
 | 
	
		
			
				|  |  |    def getOtherComment(self):
 | 
	
		
			
				|  |  |      return self.getDbComment() if mode == "xattr" else self.getXattrComment()
 | 
	
		
			
				|  |  | +  def getData(self):
 | 
	
		
			
				|  |  | +    return self.getDbData()    if mode == "db"    else self.getXattrData()
 | 
	
		
			
				|  |  | +  def getOtherData(self):
 | 
	
		
			
				|  |  | +    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)
 | 
	
		
			
				|  |  | +    if stat.S_ISREG(self.stat.st_mode):
 | 
	
		
			
				|  |  | +      dest = os.path.join(destDir,self.displayName)
 | 
	
		
			
				|  |  | +      try:
 | 
	
		
			
				|  |  | +        print("try copy from",self.fileName,"to",dest)
 | 
	
		
			
				|  |  | +        shutil.copy2(self.fileName, dest)
 | 
	
		
			
				|  |  | +      except:
 | 
	
		
			
				|  |  | +        errorBox(f"file copy to <{dest}> failed; check permissions")
 | 
	
		
			
				|  |  | +      f = FileObj(dest, self.db)
 | 
	
		
			
				|  |  | +      print("dest object created",repr(f))
 | 
	
		
			
				|  |  | +      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 +323,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 +391,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 +418,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+V")
 | 
	
		
			
				|  |  | +    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)
 | 
	
		
			
				|  |  |      
 | 
	
	
		
			
				|  | @@ -405,46 +478,28 @@ class DirNotes(QMainWindow):
 | 
	
		
			
				|  |  |      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 +507,55 @@ class DirNotes(QMainWindow):
 | 
	
		
			
				|  |  |        print_v("double click on {}".format(fo.getName()))
 | 
	
		
			
				|  |  |        self.curPath = fo.getName()
 | 
	
		
			
				|  |  |        self.refill()
 | 
	
		
			
				|  |  | +  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 copyFile(self):
 | 
	
		
			
				|  |  |      # 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 
 | 
	
		
			
				|  |  | +    if not fo.isDir() and not fo.isLink() and not fo.isSock(): 
 | 
	
		
			
				|  |  |        print_v(f"copy 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
 | 
	
		
			
				|  |  | +        print_v(f"copy to {r}")
 | 
	
		
			
				|  |  | +        fo.copyFile(r)
 | 
	
		
			
				|  |  | +  def moveFile(self):
 | 
	
		
			
				|  |  | +    # TODO: write this
 | 
	
		
			
				|  |  | +    print_v("moveFile not yet written")
 | 
	
		
			
				|  |  |        
 | 
	
		
			
				|  |  |    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()}</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 +564,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()
 | 
	
		
			
				|  |  | +      comment, auth, cdate = this_file.getData()
 | 
	
		
			
				|  |  |        other_comment = this_file.getOtherComment()
 | 
	
		
			
				|  |  | -      ci = SortableTableWidgetItem(comment,'',this_file)
 | 
	
		
			
				|  |  | -      ci.setToolTip(f"comment: {comment}\ncomment date: {this_file.getDbDate()}\nauthor: {this_file.getDbAuthor()}")
 | 
	
		
			
				|  |  | +      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)))
 | 
	
		
			
				|  |  |          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.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.setIcon(dirIcon)
 | 
	
		
			
				|  |  |        elif this_file.isLink():
 | 
	
		
			
				|  |  |          sa = SortableTableWidgetItem('symlink',-1,this_file)
 | 
	
		
			
				|  |  | -        item.setIcon(linkIcon)
 | 
	
		
			
				|  |  | +        sa.setIcon(linkIcon)
 | 
	
		
			
				|  |  |          dst = os.path.realpath(this_file.getName())
 | 
	
		
			
				|  |  |          sa.setToolTip(f"symlink: {dst}")
 | 
	
		
			
				|  |  | +      elif this_file.isSock():
 | 
	
		
			
				|  |  | +        sa = SortableTableWidgetItem('socket',-1,this_file)
 | 
	
		
			
				|  |  | +        sa.setIcon(sockIcon)
 | 
	
		
			
				|  |  |        else:
 | 
	
		
			
				|  |  |          sa = SortableTableWidgetItem(str(si),si,this_file)
 | 
	
		
			
				|  |  | -        item.setIcon(fileIcon)
 | 
	
		
			
				|  |  | +        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)
 | 
	
	
		
			
				|  | @@ -554,15 +626,20 @@ class DirNotes(QMainWindow):
 | 
	
		
			
				|  |  |      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())
 | 
	
		
			
				|  |  | +    the_file.setDbComment(str(x.text()))
 | 
	
		
			
				|  |  | +    the_file.setXattrComment(str(x.text())) 
 | 
	
		
			
				|  |  | +    if the_file.getComment() == the_file.getOtherComment():
 | 
	
		
			
				|  |  | +      x.setBackground(QBrush(QColor(255,255,255)))
 | 
	
		
			
				|  |  | +    else:
 | 
	
		
			
				|  |  | +      x.setBackground(QBrush(QColor(255,255,160)))
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |    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):
 | 
	
	
		
			
				|  | @@ -637,10 +714,20 @@ if __name__=="__main__":
 | 
	
		
			
				|  |  |    xattr_author  = xattr_comment + ".author"
 | 
	
		
			
				|  |  |    xattr_date    = xattr_comment + ".date"
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -  mode = "xattr" if p.xattr else "db"
 | 
	
		
			
				|  |  | +  mode = "db"
 | 
	
		
			
				|  |  | +  if "start_mode" in config and config["start_mode"] in modes:
 | 
	
		
			
				|  |  | +    mode = config["start_mode"] 
 | 
	
		
			
				|  |  | +  if p.xattr:
 | 
	
		
			
				|  |  | +    mode = "xattr" 
 | 
	
		
			
				|  |  | +  if p.db:
 | 
	
		
			
				|  |  | +    mode = "db"
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |    a = QApplication([])
 | 
	
		
			
				|  |  | -  mainWindow = DirNotes(p.dirname,db,config["start_mode"])
 | 
	
		
			
				|  |  | +  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 +737,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 +746,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
 | 
	
		
			
				|  |  | +'''
 |