| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744 | #!/usr/bin/python3# TODO: pick up comment for cwd and display at the top somewhere, or maybe status line""" a simple gui or command line appto view and create/edit file commentscomments are stored in an SQLite3 database  default ~/.dirnotes.dbwhere possible, comments are duplicated in   xattr user.xdg.comment  some file systems don't allow xattr, and even linux  doesn't allow xattr on symlinks, so the database is  considered primary  these comments stick to the symlink, not the derefnav tools are enabled, so you can double-click to go into a dir"""VERSION = "0.4"helpMsg = f"""<table width=100%><tr><td><h1>Dirnotes</h1><td><td align=right>Version: {VERSION}</td></tr></table><h3>Overview</h3>This app allows you to add comments to files. The comments are stored in a <i>database</i>, and where possible, saved in the <i>xattr</i> (hidden attributes) field of the file system.<p> Double click on a comment to create or edit.<p> You can sort the directory listing by clicking on the column heading.<p> Double click on directory names to navigate the file system. Hover overfields for more information.<h3>xattr extended attributes</h3>The xattr 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 or backedup (<i>mv, rsync</i> and <i>tar</i> work, <i>ssh</i> and <i>scp</i> don't)  <li>xattrs are not available for symlinks  <li>some programs which edit files do not preserve the xattrs during file-save (<i>vim</i>)</ul>On the other hand, <i>xattr</i> comments can be bound directly to files on removablemedia (as long as the disk format allows it).<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."""import sys,os,argparse,stat,getpass#~ from dirWidget import DirWidgetfrom PyQt5.QtGui import *from PyQt5.QtWidgets import *from PyQt5.QtCore import Qt, pyqtSignalimport sqlite3, json, timeVERSION = "0.4"xattr_comment = "user.xdg.comment"xattr_author  = "user.xdg.comment.author"xattr_date    = "user.xdg.comment.date"DATE_FORMAT   = "%Y-%m-%d %H:%M:%S"YEAR          = 3600*25*365## globalsmode_names = {"db":"<Database mode> ","xattr":"<Xattr mode>"} modes = ("db","xattr") mode = "db" global mainWindow, dbNameDEFAULT_CONFIG_FILE = "~/.dirnotes.conf"# config#    we could store the config in the database, in a second table#    or in a .json fileDEFAULT_CONFIG = {"xattr_tag":"user.xdg.comment",  "database":"~/.dirnotes.db",  "start_mode":"xattr",  "options for database":("~/.dirnotes.db","/etc/dirnotes.db"),  "options for start_mode":("db","xattr")}verbose = Nonedef print_v(*a):  if verbose:    print(*a)class dnDataBase:  ''' the database is flat    fileName: fully qualified name    st_mtime: a float    size: a long    comment: a string    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  '''  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")      raise    self.db_cursor = self.db.cursor()    try:      self.db_cursor.execute("select * from dirnotes")    except sqlite3.OperationalError:      print_v("Table %s created" % ("dirnotes"))      self.db_cursor.execute("create table dirnotes (name TEXT, date DATETIME, size INTEGER, comment TEXT, comment_date DATETIME, author TEXT)")      def getData(self, fileName):    self.db_cursor.execute("select * from dirnotes where name=? and comment<>'' order by comment_date desc",(os.path.abspath(fileName),))    return self.db_cursor.fetchone()  def setData(self, fileName, comment):    s = os.lstat(fileName)    print_v ("params: %s %s %d %s %s" % ((os.path.abspath(fileName),         dnDataBase.epochToDb(s.st_mtime), s.st_size, comment,         dnDataBase.epochToDb(time.time()))))    try:      self.db_cursor.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(), getpass.getuser()))      self.db.commit()    except sqlite3.OperationalError:      print("database is locked or unwriteable")      errorBox("database is locked or unwriteable")      #TODO: put up a message box for locked database  @staticmethod  def epochToDb(epoch):    return time.strftime(DATE_FORMAT,time.localtime(epoch))  @staticmethod  def DbToEpoch(dbTime):    return time.mktime(time.strptime(dbTime,DATE_FORMAT))  @staticmethod  def getShortDate(longDate):    now = time.time()    diff = now - longDate    if diff > YEAR:      fmt = "%b %e %Y"    else:      fmt = "%b %d %H:%M"    return time.strftime(fmt, time.localtime(longDate))    ## one for each file## and a special one for ".." parent directoryclass FileObj():  FILE_IS_DIR    = -1  FILE_IS_LINK   = -2  FILE_IS_SOCKET = -3  def __init__(self, fileName):    self.fileName = os.path.abspath(fileName)    self.displayName = os.path.split(fileName)[1]    s = os.lstat(self.fileName)    self.date = s.st_mtime    if stat.S_ISDIR(s.st_mode):      self.size = FileObj.FILE_IS_DIR      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  def getName(self):    return self.fileName  def getFileName(self):    return self.displayName  # with an already open database cursor  def loadDbComment(self,cursor):    cursor.execute("select comment,author,comment_date from dirnotes where name=? and comment<>'' order by comment_date desc",(self.fileName,))    a = cursor.fetchone()    if a:      self.dbComment, self.dbAuthor, self.dbDate = a      self.commentsDiffer = True if self.xattrComment == self.dbComment else False  def getDbComment(self):    return self.dbComment  def getDbAuthor(self):    return self.dbAuthor  def getDbDate(self):    return self.dbDate  def setDbComment(self,newComment):    try:      self.db = sqlite3.connect(dbName)    except sqlite3.OperationalError:      print_v(f"database {dbName} not found")      raise    c = self.db.cursor()    s = os.lstat(self.fileName)    try:      c.execute("insert into dirnotes (name,date,size,comment,comment_date,author) values (?,datetime(?,'unixepoch','localtime'),?,?,datetime(?,'unixepoch','localtime'),?)",          (os.path.abspath(self.fileName), s.st_mtime, s.st_size,          str(newComment), time.time(), getpass.getuser()))      self.db.commit()      print_v(f"database write for {self.fileName}")      self.dbComment = newComment    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      return True    # we need to move these cases out to a handler     except Exception as e:      if self.size == FileObj.FILE_IS_LINK:        errorBox("Linux does not allow xattr comments on symlinks; comment is stored in database")      elif self.size == FileObj.FILE_IS_SOCKET:        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):    return self.getDbComment() if mode == "xattr" else self.getXattrComment()  def getDate(self):    return self.date  def getSize(self):    return self.size  def isDir(self):    return self.size == self.FILE_IS_DIR  def isLink(self):    return self.size == self.FILE_IS_LINKclass 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)    self.show()class errorBox(QDialog):  def __init__(self, text):    print_v(f"errorBox: {text}")    super(QDialog, self).__init__(mainWindow)    self.layout = QVBoxLayout(self)    self.tb = QLabel(self)    self.tb.setWordWrap(True)    self.tb.setFixedWidth(500)    self.tb.setText(text)    self.pb = QPushButton("OK",self)    self.layout.addWidget(self.tb)    self.layout.addWidget(self.pb)    self.pb.clicked.connect(self.close)    self.show()icon = ["32 32 6 1",  # the QPixmap constructor allows for str[]"   c None",".  c #666666","+  c #FFFFFF","@  c #848484","#  c #000000","$  c #FCE883","                                ","  ........                      "," .++++++++.                     "," .+++++++++..................   "," .+++++++++++++++++++++++++++.  "," .+++++++++++++++++++++++++++.  "," .++..+......++@@@@@@@@@@@@@@@@@"," .++..++++++++#################@"," .+++++++++++#$$$$$$$$$$$$$$$$$#"," .++..+.....+#$$$$$$$$$$$$$$$$$#"," .++..+++++++#$$$$$$$$$$$$$$$$$#"," .+++++++++++#$$#############$$#"," .++..+.....+#$$$$$$$$$$$$$$$$$#"," .++..+++++++#$$########$$$$$$$#"," .+++++++++++#$$$$$$$$$$$$$$$$$#"," .++..+.....+#$$$$$$$$$$$$$$$$$#"," .++..++++++++#######$$$####### "," .++++++++++++++++++#$$#++++++  "," .++..+............+#$#++++++.  "," .++..++++++++++++++##+++++++.  "," .++++++++++++++++++#++++++++.  "," .++..+............++++++++++.  "," .++..+++++++++++++++++++++++.  "," .+++++++++++++++++++++++++++.  "," .++..+................++++++.  "," .++..+++++++++++++++++++++++.  "," .+++++++++++++++++++++++++++.  "," .++..+................++++++.  "," .++..+++++++++++++++++++++++.  "," .+++++++++++++++++++++++++++.  ","  ...........................   ","                                "]''' a widget that shows only a dir listing '''class DirWidget(QListWidget):  ''' a simple widget that shows a list of directories, staring  at the directory passed into the constructor  a mouse click or 'enter' key will send a 'selected' signal to  anyone who connects to this.  the .. parent directory is shown for all dirs except /  the most interesting parts of the interface are:  constructor - send in the directory to view  method - currentPath() returns text of current path  signal - selected calls a slot with a single arg: new path  '''  selected = pyqtSignal(str)  def __init__(self, directory='.', parent=None):    super(DirWidget,self).__init__(parent)    self.directory = directory    self.refill()    self.itemActivated.connect(self.selectionByLWI)    # it would be nice to pick up single-mouse-click for selection as well    # but that seems to be a system-preferences global  def selectionByLWI(self, li):    self.directory = os.path.abspath(self.directory + '/' + str(li.text()))    self.refill()    self.selected.emit(self.directory)  def refill(self):    current,dirs,files = next(os.walk(self.directory,followlinks=True))    dirs.sort()    if '/' not in dirs:      dirs = ['..'] + dirs    self.clear()    for d in dirs:      li = QListWidgetItem(d,self)  def currentPath(self):    return self.directory    # 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 hereclass SortableTableWidgetItem(QTableWidgetItem):  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        class DirNotes(QMainWindow):  ''' the main window of the app    '''  def __init__(self, argFilename, db, start_mode, 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):      self.curPath = longPathName      filename = ''    else:      self.curPath, filename = os.path.split(longPathName)    print_v("working on <"+self.curPath+"> and <"+filename+">")        layout = QVBoxLayout()    copyIcon = QIcon.fromTheme('drive-harddisk-symbolic')    changeIcon = QIcon.fromTheme('emblem-synchronizing-symbolic')    topLayout = QHBoxLayout()    self.modeShow = QLabel(win)    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(lb)    win.setLayout(layout)        lb.itemChanged.connect(self.change)    lb.cellDoubleClicked.connect(self.double)    bmode.clicked.connect(self.switchMode)    cf.clicked.connect(self.copyFile)    lb.setHorizontalHeaderItem(0,QTableWidgetItem("File"))    lb.setHorizontalHeaderItem(1,QTableWidgetItem("Date/Time"))    lb.setHorizontalHeaderItem(2,QTableWidgetItem("Size"))        lb.setHorizontalHeaderItem(3,QTableWidgetItem("Comment"))    lb.setSortingEnabled(True)    self.refill()    lb.resizeColumnsToContents()        if len(filename)>0:      for i in range(lb.rowCount()):        if filename == lb.item(i,0).data(32).getFileName():          lb.setCurrentCell(i,3)          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")    self.lb.sortItems(1,Qt.DescendingOrder)  def sbs(self):    print("sort by size")    self.lb.sortItems(2)  def sbn(self):    print("sort by name")    self.lb.sortItems(0)  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    if col==0 and fo.isDir():      print_v("double click on {}".format(fo.getName()))      self.curPath = fo.getName()      self.refill()  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       print_v(f"copy file {fo.getName()}")      # open the dir.picker      # TODO: move all this out to a function      qd = QDialog(self.parent)      dw = DirWidget('.',qd)      d_ok = QPushButton('select')      d_ok.setDefault(True)      d_ok.clicked.connect(QDialog.accept)      d_nope = QPushButton('cancel')      d_nope.clicked.connect(QDialog.reject)      # if returns from <enter>, copy the file and comments      r = qd.exec()       print_v(f"copy to {r}")    pass        def refill(self):    self.refilling = True    self.lb.sortingEnabled = False        (self.modeShow.setText("View and edit file comments stored in extended attributes\n(xattr: user.xdg.comment)")       if mode=="xattr" else       self.modeShow.setText("View and edit file comments stored in the database \n(~/.dirnotes.db)"))    self.lb.clearContents()    small_font = QFont("",8)    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))    dirs.sort()    files.sort()        if current != '/':      dirs.insert(0,"..")    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(current+'/'+name)      this_file.loadDbComment(self.db.db_cursor)      print_v("FileObj created as {} and the db-comment is <{}>".format(this_file.displayName, 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]      display_name = this_file.getFileName()      if this_file.getSize() == FileObj.FILE_IS_DIR:        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())      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 = QTableWidgetItem(comment)      ci.setToolTip(f"comment: {comment}\ncomment date: {this_file.getDbDate()}\nauthor: {this_file.getDbAuthor()}")      if other_comment != comment:        ci.setBackground(QBrush(QColor(255,255,160)))        print_v("got differing comments <{}> and <{}>".format(comment, other_comment))      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)))      self.lb.setItem(i,1,da)      si = this_file.getSize()      if this_file.isDir():        sa = SortableTableWidgetItem('',0,this_file)        item.setIcon(dirIcon)      elif this_file.isLink():        sa = SortableTableWidgetItem('symlink',-1,this_file)        item.setIcon(linkIcon)        dst = os.path.realpath(this_file.getName())        sa.setToolTip(f"symlink: {dst}")      else:        sa = SortableTableWidgetItem(str(si),si,this_file)        item.setIcon(fileIcon)      sa.setTextAlignment(Qt.AlignRight)      self.lb.setItem(i,2,sa)    self.refilling = False    self.lb.sortingEnabled = True    self.lb.resizeColumnToContents(1)        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(str(x.text()))    r = the_file.setXattrComment(str(x.text()))     # TODO: change this to FileObj.setDbComment()    if r:      self.db.setData(the_file.getName(),x.text())  def switchMode(self):    global mode    mode = "xattr" if mode == "db" else "db"     self.refill()  # 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')  parser.add_argument('dirname',metavar='dirname',type=str,    help='directory or file [default=current dir]',default=".",nargs='?')  #parser.add_argument('dirname2',help='comparison directory, shows two-dirs side-by-side',nargs='?')  parser.add_argument('-V','--version',action='version',version='%(prog)s '+VERSION)  parser.add_argument('-v','--verbose',action='count',help="verbose, almost debugging")  group = parser.add_mutually_exclusive_group()  group.add_argument( '-s','--sort-by-size',action='store_true')  group.add_argument( '-m','--sort-by-date',action='store_true')  parser.add_argument('-c','--config', dest='config_file',help="config file (json format; default ~/.dirnotes.json)")  parser.add_argument('-x','--xattr', action='store_true',help="start up in xattr mode")  parser.add_argument('-d','--db',    action='store_true',help="start up in database mode (default)")    return parser.parse_args()if __name__=="__main__":  # TODO: delete this after debugging  #from PyQt5.QtCore import pyqtRemoveInputHook  #pyqtRemoveInputHook()  p = parse()  if len(p.dirname)>1 and p.dirname[-1]=='/':    p.dirname = p.dirname[:-1]  if os.path.isdir(p.dirname):    p.dirname = p.dirname + '/'  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}")    print_v(f"here is the .json {repr(config)}")  dbName = os.path.expanduser(config["database"])  db = dnDataBase(dbName)  xattr_comment = config["xattr_tag"]  xattr_author  = xattr_comment + ".author"  xattr_date    = xattr_comment + ".date"  mode = "xattr" if p.xattr else "db"  a = QApplication([])  mainWindow = DirNotes(p.dirname,db,config["start_mode"])  mainWindow.show()    if p.sort_by_date:    mainWindow.sbd()  if p.sort_by_size:    mainWindow.sbs()  a.exec_()    #xattr.setxattr(filename,COMMENT_KEY,commentText)''' files from directoriesuse os.isfile()os.isdir()current, dirs, files = os.walk("path").next()possible set folllowLinks=True'''''' notes from the wdrm projecttable showed filename, size, date size, date, descat start, fills the list of all the filesskip the . entry'''''' should we also do user.xdg.tags="TagA,TagB" ?user.charsetuser.creator=application_name or user.xdg.creatoruser.xdg.origin.urluser.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 xattrgetfattr -h (don't follow symlink) -d (dump all properties)'''''' if the args line contains a file, jump to it '''
 |