#!/usr/bin/python3 # 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 # TODO: change banner line to indicate the CWD """ a simple gui or command line app to view and create/edit file comments comments are stored in an SQLite3 database default ~/.local/share/dirnotes/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 you can double-click to go into a dir """ VERSION = "0.9" helpMsg = f"""

Dirnotes

Version: {VERSION}

Overview

This app allows you to add comments to files. The comments are stored in a database, and where possible, saved in the xattr (hidden attributes) field of the file system.

Double click on a comment to create or edit.

You can sort the directory listing by clicking on the column heading.

Double click on directory names to navigate the file system. Hover over fields for more information.

xattr extended attributes

The xattr comment suffers from a few problems: On the other hand, xattr comments can be bound directly to files on removable media (as long as the disk format allows it).

database comments

Database comments can be applied to any file, even read-only files and executables.

When the database version of a comment differs from the xattr version, the comment box gets a light yellow background.

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,argparse,logging from PyQt5.QtGui import * from PyQt5.QtWidgets import * from PyQt5.QtCore import Qt, pyqtSignal 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":" ","xattr":""} modes = ("db","xattr") mode = "db" global mainWindow, dbName verbose = None def print_d(*a): if verbose: print(*a) # >>> snip here <<< #============ the DnDataBase, UiHelper and FileObj code is shared with other dirnotes programs import getpass, time, stat, shutil, sqlite3, json, os, math DEFAULT_CONFIG_FILE = "~/.config/dirnotes/dirnotes.conf" # or /etc/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":"~/.local/share/dirnotes/dirnotes.db", "start_mode":"xattr", "options for database":("~/.local/share/dirnotes/dirnotes.db","~/.dirnotes.db","/etc/dirnotes.db"), "options for start_mode":("db","xattr") } class ConfigLoader: # singleton def __init__(self, configFile): configFile = os.path.expanduser(configFile) try: with open(configFile,"r") as f: config = json.load(f) except json.JSONDecodeError: errorBox(f"problem reading config file {configFile}; check the JSON syntax") config = DEFAULT_CONFIG except FileNotFoundError: errorBox(f"config file {configFile} not found; using the default settings") config = DEFAULT_CONFIG try: os.makedirs(os.path.dirname(configFile),exist_ok = True) with open(configFile,"w") as f: json.dump(config,f,indent=4) except: errorBox(f"problem creating the config file {configFile}") self.dbName = os.path.expanduser(config["database"]) self.mode = config["start_mode"] # can get over-ruled by the command line options self.xattr_comment = config["xattr_tag"] class DnDataBase: ''' the database is flat fileName: fully qualified name st_mtime: a float size: a long comment: a string comment_time: a float, the time of the comment save author: the username that created the comment 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_d(f"Database {dbFile} not found") try: os.makedirs(os.path.dirname(dbFile), exist_ok = True) self.db = sqlite3.connect(dbFile) except (sqlite3.OperationalError, PermissionError): printd(f"Failed to create {dbFile}, aborting") raise # create new table if it doesn't exist try: self.db.execute("select * from dirnotes") except sqlite3.OperationalError: self.db.execute("create table dirnotes (name TEXT, date DATETIME, size INTEGER, comment TEXT, comment_date DATETIME, author TEXT)") self.db.execute("create index dirnotes_i on dirnotes(name)") print_d(f"Table dirnotes created") # 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 DATE_FORMAT = "%Y-%m-%d %H:%M:%S" class UiHelper: @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 %e %H:%M" return time.strftime(fmt, time.localtime(longDate)) @staticmethod def getShortSize(fo): if fo.isDir(): return "

" elif fo.isLink(): return " " size = fo.getSize() log = int((math.log10(size+1)-2)/3) s = " KMGTE"[log] base = int(size/math.pow(10,log*3)) return f"{base}{s}".strip().rjust(7) ## one for each file ## and a special one for ".." parent directory class FileObj: """ 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 += '/' self.date = self.stat.st_mtime self.size = self.stat.st_size self.db = db def getName(self): """ returns the absolute pathname """ return self.fileName def getDisplayName(self): """ returns just the basename of the file; dirs end in / """ return self.displayName def getDbData(self): """ returns (comment, author, comment_date) """ if not hasattr(self,'dbCommentAuthorDate'): cad = self.db.execute("select comment, author, comment_date from dirnotes where name=? order by comment_date desc",(self.fileName,)).fetchone() self.dbCommentAuthorDate = cad if cad else (None, None, None) return self.dbCommentAuthorDate def getDbComment(self): return self.getDbData()[0] def getXattrData(self): """ returns (comment, author, comment_date) """ if not hasattr(self,'xattrCommentAuthorDate'): c = a = d = None try: c = os.getxattr(self.fileName, xattr_comment, follow_symlinks=False).decode() a = os.getxattr(self.fileName, xattr_author, follow_symlinks=False).decode() d = os.getxattr(self.fileName, xattr_date, follow_symlinks=False).decode() except: # no xattr comment pass self.xattrCommentAuthorDate = c,a,d return self.xattrCommentAuthorDate def getXattrComment(self): return self.getXattrData()[0] def setDbComment(self,newComment): # how are we going to hook this? #if not self.db.writable: # errorBox("The database is readonly; you cannot add or edit comments") # return s = os.lstat(self.fileName) try: print_d(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())) self.db.commit() self.dbCommentAuthorDate = newComment, getpass.getuser(), UiHelper.epochToDb(time.time()) except sqlite3.OperationalError: print_d("database is locked or unwritable") errorBox("the database that stores comments is locked or unwritable") def setXattrComment(self,newComment): print_d(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.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.isLink(): errorBox("Linux does not allow xattr comments on symlinks; comment is stored in database") 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(f"you don't appear to have write permissions on this file: {self.fileName}") # change the listbox background to yellow elif "Errno 95" in str(e): errorBox("is this a VFAT or EXFAT volume? these don't allow comments") return False def getComment(self,mode): """ returns the comment for the given mode """ return self.getDbComment() if mode == "db" else self.getXattrComment() def getOtherComment(self,mode): return self.getDbComment() if mode == "xattr" else self.getXattrComment() def getData(self,mode): """ returns (comment, author, comment_date) for the given mode """ return self.getDbData() if mode == "db" else self.getXattrData() def getOtherData(self,mode): """ returns (comment, author, comment_date) for the 'other' mode """ 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 stat.S_ISDIR(self.stat.st_mode) def isLink(self): return stat.S_ISLNK(self.stat.st_mode) def isSock(self): return stat.S_ISSOCK(self.stat.st_mode) def copyFile(self, dest, doMove = False): """ dest is either a FQ filename or a FQ directory, to be expanded with same basename """ # NOTE: this method copies the xattr (comment + old author + old date) # but creates new db (comment + this author + new date) if os.path.isdir(dest): dest = os.path.join(destDir,self.displayName) try: print_d("try copy from",self.fileName,"to",dest) # shutil methods preserve dates & chmod/chown & xattr if doMove: shutil.move(self.fileName, dest) else: shutil.copy2(self.fileName, dest) # can raise FileNotFoundError, Permission Error, shutil.SameFileError, IsADirectoryError except: errorBox(f"file copy/move to <{dest}> failed; check permissions") return # and copy the database record f = FileObj(dest, self.db) f.setDbComment(self.getDbComment()) def moveFile(self, dest): """ dest is either a FQ filename or a FQ directory, to be expanded with same basename """ self.copyFile(dest, doMove = True) # >>> snip here <<< class HelpWidget(QDialog): def __init__(self, parent): super(QDialog, self).__init__(parent) 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 = QVBoxLayout(self) layout.addWidget(tb) lowerBox = QHBoxLayout() 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) tb = QLabel(self) tb.setWordWrap(True) tb.setText(keyboardHelpMsg) tb.setFixedWidth(500) pb = QPushButton('OK',self) layout = QVBoxLayout(self) layout.addWidget(tb) layout.addWidget(pb) pb.clicked.connect(self.close) self.show() class errorBox(QDialog): def __init__(self, text): print_d(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() keyboardHelpMsg = """

Keyboard Shortcuts

Arrowsnormal movement through the table
Ctrl+Nsort the listing by filename
Ctrl+Dsort the listing by date
Ctrl+Ssort the listing by size
Ctrl+Tsort the listing by comment
Ctrl+Mtoggle between database and xattr views
Alt+C copy the file and its comments
Alt+M copy the file and its comments
1st column: any letterjump to file beginning with that letter
1st column: <Enter> change directory
4th column: any lettercreate a comment; replace any existing comment
4th column: <Enter> open an existing comment for edit
Ctrl+Qquit the app

NOTE: In edit mode, Ctrl+X, Ctrl+C and Ctrl+V work for cut, copy and paste. """ icon = ["32 32 6 1", # the QPixmap constructor allows for str[] " c None", ". c #666666", "+ c #FFFFFF", "@ c #848484", "# c #000000", "$ c #FCE883", " ", " ........ ", " .++++++++. ", " .+++++++++.................. ", " .+++++++++++++++++++++++++++. ", " .+++++++++++++++++++++++++++. ", " .++..+......++@@@@@@@@@@@@@@@@@", " .++..++++++++#################@", " .+++++++++++#$$$$$$$$$$$$$$$$$#", " .++..+.....+#$$$$$$$$$$$$$$$$$#", " .++..+++++++#$$$$$$$$$$$$$$$$$#", " .+++++++++++#$$#############$$#", " .++..+.....+#$$$$$$$$$$$$$$$$$#", " .++..+++++++#$$########$$$$$$$#", " .+++++++++++#$$$$$$$$$$$$$$$$$#", " .++..+.....+#$$$$$$$$$$$$$$$$$#", " .++..++++++++#######$$$####### ", " .++++++++++++++++++#$$#++++++ ", " .++..+............+#$#++++++. ", " .++..++++++++++++++##+++++++. ", " .++++++++++++++++++#++++++++. ", " .++..+............++++++++++. ", " .++..+++++++++++++++++++++++. ", " .+++++++++++++++++++++++++++. ", " .++..+................++++++. ", " .++..+++++++++++++++++++++++. ", " .+++++++++++++++++++++++++++. ", " .++..+................++++++. ", " .++..+++++++++++++++++++++++. ", " .+++++++++++++++++++++++++++. ", " ........................... ", " "] # 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 + 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, parent=None): super(DirNotes,self).__init__(parent) self.db = db self.refilling = False self.parent = parent longPathName = os.path.abspath(argFilename) print_d("longpathname is {}".format(longPathName)) if os.path.isdir(longPathName): self.curPath = longPathName filename = '' else: self.curPath, filename = os.path.split(longPathName) print_d("working on <"+self.curPath+"> and <"+filename+">") win = QWidget() self.setCentralWidget(win) 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+S") mf.addAction("Sort by comment", self.sbc, "Ctrl+T") mf.addSeparator() mf.addAction("Change mode", self.switchMode, "Ctrl+M") mf.addAction("Copy file", self.copyFile, "Alt+C") mf.addAction("Move file", self.moveFile, "Alt+M") mf.addSeparator() mf.addAction("Quit", self.close, QKeySequence.Quit) mf.addAction("About", self.about, QKeySequence.HelpContents) self.setWindowTitle("==DirNotes== Dir: "+self.curPath) self.setMinimumSize(600,700) self.setWindowIcon(QIcon(QPixmap(icon))) 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) topLayout.addWidget(bmode) topLayout.addWidget(cf) layout.addLayout(topLayout) layout.addWidget(self.thisDirLabel) 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.sameBrush = QBrush(QColor(255,255,255)) self.differBrush = QBrush(QColor(255,255,160)) self.refill() lb.resizeColumnsToContents() if filename: for i in range(lb.rowCount()): if filename == lb.item(i,0).file_object.getDisplayName(): lb.setCurrentCell(i,0) break lb.setFocus() def sbd(self): print_d("sort by date") self.lb.sortItems(1,Qt.DescendingOrder) def sbs(self): print_d("sort by size") self.lb.sortItems(2) def sbn(self): print_d("sort by name") self.lb.sortItems(0) def sbc(self): print_d("sort by comment") self.lb.sortItems(3) def about(self): HelpWidget(self) def double(self,row,col): print_d("double click {} {}".format(row, col)) fo = self.lb.item(row,0).file_object if col==0 and fo.isDir(): print_d("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 copyMoveFile(self, doCopy, pickerTitle): # get current selection r, c = self.lb.currentRow(), self.lb.currentColumn() fo = self.lb.item(r,c).file_object if not fo.isDir() and not fo.isLink() and not fo.isSock(): print_d(f"{'copy' if doCopy=='copy' else 'move'} file {fo.getName()}") # open the dir.picker d = QFileDialog.getExistingDirectory(self.parent, pickerTitle) if d: print_d(f"senf file to {d}") fo.copyFile(d) if doCopy=='copy' else fo.moveFile(d) def copyFile(self): self.copyMoveFile('copy',"Select destination for FileCopy") def moveFile(self): self.copyMoveFile('move',"Select destination for FileMove") self.refill() def refill(self): self.refilling = True self.lb.sortingEnabled = False self.directory = FileObj(self.curPath,self.db) self.thisDirLabel.setText(f'
{self.directory.getDisplayName()}{self.directory.getComment(mode)}
') (self.modeShow.setText("Showing comments stored in extended attributes
(xattr: user.xdg.comment)") if mode=="xattr" else self.modeShow.setText("Showing comments from the database
(~/.dirnotes.db)")) self.lb.clearContents() dirIcon = QIcon.fromTheme('folder') fileIcon = QIcon.fromTheme('text-x-generic') linkIcon = QIcon.fromTheme('emblem-symbolic-link') 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() if current != '/': dirs.insert(0,"..") d = dirs + files self.lb.setRowCount(len(d)) #~ print("insert {} items into cleared table {}".format(len(d),current)) for i,name in enumerate(d): this_file = FileObj(os.path.join(current,name),self.db) print_d("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())) display_name = this_file.getDisplayName() 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.setToolTip(this_file.getName()) item.setFlags(Qt.ItemIsEnabled) self.lb.setItem(i,0,item) # get the comment from database & xattrs, either can fail comment, auth, cdate = this_file.getData(mode) other_comment = this_file.getOtherComment(mode) ci = SortableTableWidgetItem(comment,comment or '~',this_file) ci.setToolTip(f"comment: {comment}\ncomment date: {cdate}\nauthor: {auth}") if other_comment != comment: ci.setBackground(self.differBrush) print_d("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(UiHelper.getShortDate(dt),dt,this_file) da.setToolTip(time.strftime(DATE_FORMAT,time.localtime(dt))) da.setFlags(Qt.ItemIsEnabled) self.lb.setItem(i,1,da) si = this_file.getSize() if this_file.isDir(): sa = SortableTableWidgetItem(UiHelper.getShortSize(this_file),0,this_file) sa.setIcon(dirIcon) elif this_file.isLink(): sa = SortableTableWidgetItem(UiHelper.getShortSize(this_file),-1,this_file) sa.setIcon(linkIcon) dst = os.path.realpath(this_file.getName()) sa.setToolTip(f"symlink: {dst}") elif this_file.isSock(): sa = SortableTableWidgetItem(UiHelper.getShortSize(this_file),-1,this_file) sa.setIcon(sockIcon) else: sa = SortableTableWidgetItem(UiHelper.getShortSize(this_file),si,this_file) sa.setIcon(fileIcon) sa.setTextAlignment(Qt.AlignRight) sa.setFlags(Qt.ItemIsEnabled) self.lb.setItem(i,2,sa) self.lb.setCurrentCell(0,0) self.refilling = False self.lb.sortingEnabled = True self.lb.resizeColumnToContents(1) def change(self,x): if self.refilling: return the_file = self.lb.item(x.row(),0).file_object print_d(f"debugging {x.text()} r:{str(x.row())} c:{str(x.column())}") print_d(f" selected file: {the_file.getName()} new text: >{x.text()}<") the_file.setDbComment(str(x.text())) the_file.setXattrComment(str(x.text())) # set the background (wrap it, because of reentry to .change()) self.refilling = True if the_file.getComment(mode) == the_file.getOtherComment(mode): x.setBackground(self.sameBrush) else: x.setBackground(self.differBrush) self.refilling = False def switchMode(self): global mode mode = "xattr" if mode == "db" else "db" row,column = self.lb.currentRow(), self.lb.currentColumn() self.refill() self.lb.setCurrentCell(row,column) self.lb.setFocus(True) 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_d(f"using {p.dirname}") verbose = p.verbose config = ConfigLoader(p.config_file or DEFAULT_CONFIG_FILE) print_d(f"here is the .json {repr(config)}") dbName = config.dbName db = DnDataBase(dbName).db xattr_comment = config.xattr_comment xattr_author = xattr_comment + ".author" xattr_date = xattr_comment + ".date" mode = config.mode if p.xattr: mode = "xattr" if p.db: mode = "db" a = QApplication([]) # TODO: add 'mode' as an argument to contructor; add setMode() as a method mainWindow = DirNotes(p.dirname,db) if p.sort_by_size: mainWindow.sbs() if p.sort_by_date: mainWindow.sbd() mainWindow.show() a.exec_() ''' 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: also need a way to display-&-restore comments from the database ''' ''' TODO: implement startup -s and -m for size and date ''' ''' TODO: create 'show comment history' popup ''' ''' commandline xattr getfattr -h (don't follow symlink) -d (dump all properties) ''' ''' 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 '''