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 app
- to view and create/edit file comments
- comments are stored in an SQLite3 database
- default ~/.dirnotes.db
- where 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 deref
- nav 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 over
- fields 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 removable
- media (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 DirWidget
- from PyQt5.QtGui import *
- from PyQt5.QtWidgets import *
- from PyQt5.QtCore import Qt, pyqtSignal
- import sqlite3, json, time
- VERSION = "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
- ## globals
- mode_names = {"db":"<Database mode> ","xattr":"<Xattr mode>"}
- modes = ("db","xattr")
- mode = "db"
- global mainWindow, dbName
- DEFAULT_CONFIG_FILE = "~/.dirnotes.conf"
- # config
- # we could store the config in the database, in a second table
- # or in a .json file
- DEFAULT_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 = None
- def 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 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)
- 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_LINK
- 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)
- 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 here
- class 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 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
- user.creator=application_name or user.xdg.creator
- user.xdg.origin.url
- 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 '''
|