dirnotes 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603
  1. #!/usr/bin/python3
  2. # TODO: add author to access
  3. # TODO: convert dates to strings
  4. """ a simple gui or command line app
  5. to view and create/edit file comments
  6. comments are stored in an SQLite3 database
  7. default ~/.dirnotes.db
  8. where possible, comments are duplicated in
  9. xattr user.xdg.comment
  10. some file systems don't allow xattr, and even linux
  11. doesn't allow xattr on symlinks, so the database is
  12. considered primary
  13. these comments stick to the symlink, not the deref
  14. nav tools are enabled, so you can double-click to go into a dir, and
  15. there's an UP arrow to navigate back
  16. """
  17. helpMsg = """<h1>Dirnotes</h1><h3>Overview</h3>
  18. This app allows you to add comments to files. The comments are stored in
  19. a database, and where possible, saved in the xattr (hidden attributes)
  20. field of the file system.
  21. <p>
  22. You can sort the directory listing by clicking on the column heading.
  23. <p>
  24. Double click on directory names to navigate the file system. Hover over
  25. fields for more information.
  26. <p>
  27. <h3>xattr extended attributes</h3>
  28. The xattr property is handy, but suffers from a few problems:
  29. <ul>
  30. <li>is not implemented on FAT/VFAT/EXFAT file systems (like USB sticks)
  31. <li>xattrs are not (by default) copied with the file when it's duplicated
  32. or backedup
  33. <li>xattrs are not available for symlinks
  34. <li>many programs which edit files do not preserve the xattrs during file-save
  35. </ul>
  36. When the database version of a comment differs from the xattr version, the
  37. comment box gets a light yellow background.
  38. """
  39. import sys,os,argparse,stat
  40. #~ from dirWidget import DirWidget
  41. from PyQt5.QtGui import *
  42. from PyQt5.QtWidgets import *
  43. from PyQt5.QtCore import Qt, pyqtSignal
  44. import sqlite3, time
  45. VERSION = "0.3"
  46. COMMENT_KEY = "user.xdg.comment"
  47. DATABASE_NAME = "~/.dirnotes.db"
  48. # convert the ~/ form to a fully qualified path
  49. DATABASE_NAME = os.path.expanduser(DATABASE_NAME)
  50. class dnDataBase:
  51. ''' the database is flat
  52. fileName: fully qualified name
  53. st_mtime: a float
  54. size: a long
  55. comment: a string
  56. comment_time: a float, the time of the comment save
  57. this is effectively a log file, as well as a resource for a restore
  58. (in case a file-move is done without comment)
  59. the database is associated with a user, in the $HOME dir
  60. '''
  61. def __init__(self):
  62. '''try to open the database; if not found, create it'''
  63. try:
  64. self.db = sqlite3.connect(DATABASE_NAME)
  65. except sqlite3.OperationalError:
  66. print("Database %s not found" % DATABASE_NAME)
  67. raise OperationalError
  68. c = self.db.cursor()
  69. try:
  70. c.execute("select * from dirnotes")
  71. except sqlite3.OperationalError:
  72. print("Table %s created" % ("dirnotes"))
  73. c.execute("create table dirnotes (name TEXT, date DATETIME, size INTEGER, comment TEXT, comment_date DATETIME)")
  74. def getData(self, fileName):
  75. c = self.db.cursor()
  76. c.execute("select * from dirnotes where name=? and comment<>'' order by comment_date desc",(os.path.abspath(fileName),))
  77. return c.fetchone()
  78. def setData(self, fileName, comment):
  79. ''' TODO: convert filename to canonical '''
  80. c = self.db.cursor()
  81. s = os.lstat(fileName)
  82. print ("params: %s %s %d %s %s" % ((os.path.abspath(fileName),
  83. dnDataBase.epochToDb(s.st_mtime), s.st_size, comment,
  84. dnDataBase.epochToDb(time.time()))))
  85. try:
  86. c.execute("insert into dirnotes values (?,datetime(?,'unixepoch','localtime'),?,?,datetime(?,'unixepoch','localtime'))",
  87. (os.path.abspath(fileName), s.st_mtime, s.st_size, str(comment), time.time()))
  88. self.db.commit()
  89. except sqlite3.OperationalError:
  90. print("database is locked or unwriteable")
  91. #TODO: put up a message box for locked database
  92. @staticmethod
  93. def epochToDb(epoch):
  94. return time.strftime("%Y-%m-%d %H:%M:%S",time.localtime(epoch))
  95. @staticmethod
  96. def DbToEpoch(dbTime):
  97. return time.mktime(time.strptime(dbTime,"%Y-%m-%d %H:%M:%S"))
  98. @staticmethod
  99. def getShortDate(longDate):
  100. e = dnDataBase.DbToEpoch(longDate)
  101. sd = time.strptime(longDate,"%Y-%m-%d %H:%M:%S")
  102. # check for this year, or today
  103. ty = time.mktime((time.localtime()[0],1,1,0,0,0,0,1,-1))
  104. today = time.mktime((time.localtime()[0:3]+(0,0,0,0,1,-1)))
  105. if e < ty:
  106. return time.strftime("%b %d %Y",sd)
  107. elif e < today:
  108. return time.strftime("%b %d",sd)
  109. else:
  110. return time.strftime("%X",sd)
  111. #~ test code for this routine
  112. #~ for i in range(int(time.time() - 370*24*3600),
  113. #~ int(time.time() + 48*3600),
  114. #~ 3599):
  115. #~ ds = dnDataBase.epochToDb(i)
  116. #~ print("%d\t%s\t%s" % (i,ds,dnDataBase.getShortDate(ds)))
  117. def parse():
  118. parser = argparse.ArgumentParser(description='dirnotes application')
  119. parser.add_argument('dirname',metavar='dirname',type=str,
  120. help='directory [default=current dir]',default=".",nargs='?')
  121. parser.add_argument('dirname2',help='comparison directory, shows two-dirs side-by-side',nargs='?')
  122. parser.add_argument('-n','--nogui',action="store_const",const="1",
  123. help='use text base interface')
  124. parser.add_argument('-v','--version',action='version',version='%(prog)s '+VERSION)
  125. group = parser.add_mutually_exclusive_group()
  126. group.add_argument('-s','--sort-by-name',metavar='sort',action="store_const",const='n')
  127. group.add_argument('-m','--sort-by-date',metavar='sort',action='store_const',const='d')
  128. return parser.parse_args()
  129. #~ class FileObj(QtCore.QObject):
  130. class FileObj():
  131. FILE_IS_DIR = -1
  132. FILE_IS_LINK = -2
  133. def __init__(self, fileName):
  134. self.fileName = fileName
  135. s = os.lstat(fileName)
  136. self.date = dnDataBase.epochToDb(s.st_mtime)
  137. if stat.S_ISDIR(s.st_mode):
  138. self.size = FileObj.FILE_IS_DIR
  139. elif stat.S_ISLNK(s.st_mode):
  140. self.size = FileObj.FILE_IS_LINK
  141. else:
  142. self.size = s.st_size
  143. self.xattrComment = ''
  144. try:
  145. self.xattrComment = os.getxattr(fileName,COMMENT_KEY,follow_symlinks=False).decode()
  146. except Exception as e:
  147. #print("comment read on %s failed, execption %s" % (self.fileName,e))
  148. pass
  149. def getName(self):
  150. return self.fileName
  151. def getFileName(self):
  152. return os.path.split(self.fileName)[1]
  153. def getXattrComment(self):
  154. return self.xattrComment
  155. def setXattrComment(self,newComment):
  156. self.xattrComment = newComment
  157. try:
  158. os.setxattr(self.fileName,COMMENT_KEY,bytes(self.xattrComment,'utf8'),follow_symlinks=False)
  159. return True
  160. # we need to move these cases out to a handler
  161. except Exception as e:
  162. print("problem setting the comment on file %s" % (self.fileName,))
  163. print("error "+repr(e))
  164. ## todo: elif file.is_sym() the kernel won't allow comments on symlinks....stored in database
  165. if self.size == FileObj.FILE_IS_LINK:
  166. print("Linux does not allow comments on symlinks; comment is stored in database")
  167. elif os.access(self.fileName, os.W_OK)!=True:
  168. print("you don't appear to have write permissions on this file")
  169. # change the listbox background to yellow
  170. self.displayBox.notifyUnchanged()
  171. elif "Errno 95" in str(e):
  172. print("is this a VFAT or EXFAT volume? these don't allow comments")
  173. return False
  174. def getDbComment(self,db):
  175. self.dbComment = ""
  176. row = db.getData(self.fileName)
  177. if row and len(row)>1:
  178. self.dbComment = row[3]
  179. def setDbComment(self,db,comment):
  180. db.setData(self.fileName,comment)
  181. def getDate(self):
  182. return self.date
  183. def getSize(self):
  184. return self.size
  185. class HelpWidget(QDialog):
  186. def __init__(self, parent):
  187. super(QDialog, self).__init__(parent)
  188. self.layout = QVBoxLayout(self)
  189. self.tb = QLabel(self)
  190. self.tb.setWordWrap(True)
  191. self.tb.setText(helpMsg)
  192. self.tb.setFixedWidth(500)
  193. self.pb = QPushButton('OK',self)
  194. self.pb.setFixedWidth(200)
  195. self.layout.addWidget(self.tb)
  196. self.layout.addWidget(self.pb)
  197. self.pb.pressed.connect(self.close)
  198. self.show()
  199. def loadWindowIcon(): # TODO: change border/folder to dark grey, no shadow, smaller
  200. icon = ["32 32 6 1",
  201. " c None",
  202. ". c #666666",
  203. "+ c #FFFFFF",
  204. "@ c #848484",
  205. "# c #000000",
  206. "$ c #FCE883",
  207. " ",
  208. " ........ ",
  209. " .++++++++. ",
  210. " .+++++++++.................. ",
  211. " .+++++++++++++++++++++++++++. ",
  212. " .+++++++++++++++++++++++++++. ",
  213. " .++..+......++@@@@@@@@@@@@@@@@@",
  214. " .++..++++++++#################@",
  215. " .+++++++++++#$$$$$$$$$$$$$$$$$#",
  216. " .++..+.....+#$$$$$$$$$$$$$$$$$#",
  217. " .++..+++++++#$$$$$$$$$$$$$$$$$#",
  218. " .+++++++++++#$$#############$$#",
  219. " .++..+.....+#$$$$$$$$$$$$$$$$$#",
  220. " .++..+++++++#$$########$$$$$$$#",
  221. " .+++++++++++#$$$$$$$$$$$$$$$$$#",
  222. " .++..+.....+#$$$$$$$$$$$$$$$$$#",
  223. " .++..++++++++#######$$$####### ",
  224. " .++++++++++++++++++#$$#++++++ ",
  225. " .++..+............+#$#++++++. ",
  226. " .++..++++++++++++++##+++++++. ",
  227. " .++++++++++++++++++#++++++++. ",
  228. " .++..+............++++++++++. ",
  229. " .++..+++++++++++++++++++++++. ",
  230. " .+++++++++++++++++++++++++++. ",
  231. " .++..+................++++++. ",
  232. " .++..+++++++++++++++++++++++. ",
  233. " .+++++++++++++++++++++++++++. ",
  234. " .++..+................++++++. ",
  235. " .++..+++++++++++++++++++++++. ",
  236. " .+++++++++++++++++++++++++++. ",
  237. " ........................... ",
  238. " "]
  239. return QPixmap(icon)
  240. ''' a widget that shows only a dir listing
  241. '''
  242. #import sys,os,argparse
  243. #from PyQt4.QtGui import *
  244. #from PyQt4 import QtGui, QtCore
  245. class DirWidget(QListWidget):
  246. ''' a simple widget that shows a list of directories, staring
  247. at the directory passed into the constructor
  248. a mouse click or 'enter' key will send a 'selected' signal to
  249. anyone who connects to this.
  250. the .. parent directory is shown for all dirs except /
  251. the most interesting parts of the interface are:
  252. constructor - send in the directory to view
  253. method - currentPath() returns text of current path
  254. signal - selected calls a slot with a single arg: new path
  255. '''
  256. selected = pyqtSignal(str)
  257. def __init__(self, directory='.', parent=None):
  258. super(DirWidget,self).__init__(parent)
  259. self.directory = directory
  260. self.refill()
  261. self.itemActivated.connect(self.selectionByLWI)
  262. # it would be nice to pick up single-mouse-click for selection as well
  263. # but that seems to be a system-preferences global
  264. def selectionByLWI(self, li):
  265. self.directory = os.path.abspath(self.directory + '/' + str(li.text()))
  266. self.refill()
  267. self.selected.emit(self.directory)
  268. def refill(self):
  269. current,dirs,files = os.walk(self.directory,followlinks=True).next()
  270. dirs.sort()
  271. if '/' not in dirs:
  272. dirs = ['..'] + dirs
  273. self.clear()
  274. for d in dirs:
  275. li = QListWidgetItem(d,self)
  276. def currentPath(self):
  277. return self.directory
  278. # sortable TableWidgetItem, based on idea by Aledsandar
  279. # http://stackoverflow.com/questions/12673598/python-numerical-sorting-in-qtablewidget
  280. # NOTE: the QTableWidgetItem has setData() and data() which may allow data bonding
  281. # in Qt5, data() binding is more awkward, so do it here
  282. class SortableTableWidgetItem(QTableWidgetItem):
  283. def __init__(self, text, sortValue, file_object):
  284. QTableWidgetItem.__init__(self, text, QTableWidgetItem.UserType)
  285. self.sortValue = sortValue
  286. self.file_object = file_object
  287. def __lt__(self, other):
  288. return self.sortValue < other.sortValue
  289. class DirNotes(QMainWindow):
  290. ''' the main window of the app
  291. has 3 list boxes: dir_left, dir_right (may be invisible) and files
  292. '''
  293. def __init__(self, argFilename, db, parent=None):
  294. super(DirNotes,self).__init__(parent)
  295. self.db = db
  296. self.refilling = False
  297. win = QWidget()
  298. self.setCentralWidget(win)
  299. lb = QTableWidget()
  300. self.lb = lb
  301. lb.setColumnCount(4)
  302. lb.horizontalHeader().setSectionResizeMode( 3, QHeaderView.Stretch );
  303. lb.verticalHeader().setDefaultSectionSize(20); # thinner rows
  304. lb.verticalHeader().setVisible(False)
  305. # resize the comments column
  306. # and resize the parent window to match the directory size
  307. # allow multiple entries on the line at this point
  308. #d = os.listdir(p.filename[0])
  309. #d.sort()
  310. longPathName = os.path.abspath(argFilename)
  311. print("longpathname is {}".format(longPathName))
  312. if os.path.isdir(longPathName):
  313. self.curPath = longPathName
  314. filename = ''
  315. else:
  316. self.curPath, filename = os.path.split(longPathName)
  317. print("working on <"+self.curPath+"> and <"+filename+">")
  318. lb.setHorizontalHeaderItem(0,QTableWidgetItem("File"))
  319. lb.setHorizontalHeaderItem(1,QTableWidgetItem("Date/Time"))
  320. lb.setHorizontalHeaderItem(2,QTableWidgetItem("Size"))
  321. lb.setHorizontalHeaderItem(3,QTableWidgetItem("Comment"))
  322. lb.setSortingEnabled(True)
  323. self.refill()
  324. lb.resizeColumnsToContents()
  325. if len(filename)>0:
  326. for i in range(lb.rowCount()):
  327. if filename == lb.item(i,0).data(32).toPyObject().getFileName():
  328. lb.setCurrentCell(i,3)
  329. break
  330. layout = QVBoxLayout()
  331. e = QLabel("View and edit file comments stored in extended attributes user.xdg.comment",win)
  332. layout.addWidget(e)
  333. bup = QPushButton("up",win)
  334. bup.setIcon(QIcon.fromTheme('go-up')) # or go-previous
  335. bup.setFixedWidth(100)
  336. b1 = QPushButton("restore from database",win)
  337. upperLayout = QHBoxLayout()
  338. upperLayout.addWidget(bup)
  339. upperLayout.addWidget(b1)
  340. layout.addLayout(upperLayout)
  341. layout.addWidget(lb)
  342. win.setLayout(layout)
  343. lb.itemChanged.connect(self.change)
  344. lb.cellDoubleClicked.connect(self.double)
  345. b1.pressed.connect(self.restore_from_database)
  346. bup.pressed.connect(self.upOneLevel)
  347. mb = self.menuBar()
  348. mf = mb.addMenu('&File')
  349. mf.addAction("Sort by name", self.sbn, "Ctrl+N")
  350. mf.addAction("Sort by date", self.sbd, "Ctrl+D")
  351. mf.addAction("Sort by size", self.sbs, "Ctrl+Z")
  352. mf.addAction("Sort by comment", self.sbc, "Ctrl+.")
  353. mf.addAction("Restore comment from database", self.restore_from_database, "Ctrl+R")
  354. mf.addSeparator()
  355. mf.addAction("Quit", self.close, "Ctrl+Q")
  356. mf.addAction("About", self.about, "Ctrl+H")
  357. #~ QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
  358. self.setWindowTitle("DirNotes Alt-F for menu Dir: "+self.curPath)
  359. self.setMinimumSize(600,700)
  360. wi = loadWindowIcon()
  361. self.setWindowIcon(QIcon(wi))
  362. lb.setFocus()
  363. def closeEvent(self,e):
  364. print("closing")
  365. def sbd(self):
  366. print("sort by date")
  367. self.lb.sortItems(1,Qt.DescendingOrder)
  368. def sbs(self):
  369. print("sort by size")
  370. self.lb.sortItems(2)
  371. def sbn(self):
  372. print("sort by name")
  373. self.lb.sortItems(0)
  374. def about(self):
  375. HelpWidget(self)
  376. def sbc(self):
  377. print("sort by comment")
  378. self.lb.sortItems(3,Qt.DescendingOrder)
  379. def newDir(self):
  380. print("change dir to "+self.dirLeft.currentPath())
  381. def double(self,row,col):
  382. print("double click {} {}".format(row, col))
  383. fo = self.lb.item(row,0).file_object
  384. if col==0 and fo.getSize() == FileObj.FILE_IS_DIR:
  385. print("double click on {}".format(fo.getName()))
  386. self.curPath = fo.getName()
  387. self.refill()
  388. def upOneLevel(self):
  389. self.curPath = os.path.abspath(self.curPath + "/..")
  390. self.refill()
  391. def refill(self):
  392. self.refilling = True
  393. self.lb.sortingEnabled = False
  394. self.lb.clearContents()
  395. small_font = QFont("",8)
  396. dirIcon = QIcon.fromTheme('folder')
  397. fileIcon = QIcon.fromTheme('text-x-generic')
  398. linkIcon = QIcon.fromTheme('emblem-symbolic-link')
  399. current, dirs, files = next(os.walk(self.curPath,followlinks=True))
  400. dirs = list(map(lambda x:x+'/', dirs))
  401. dirs.sort()
  402. files.sort()
  403. d = dirs + files
  404. self.lb.setRowCount(len(d))
  405. #~ self.files = {}
  406. self.files = []
  407. # this is a list of all the file
  408. #~ print("insert {} items into cleared table {}".format(len(d),current))
  409. for i in range(len(d)):
  410. this_file = FileObj(current+'/'+d[i])
  411. this_file.getDbComment(self.db)
  412. print("FileObj created as {} and the db-comment is <{}>".format(this_file.fileName, this_file.dbComment))
  413. #~ print("insert order check: {} {} {} {}".format(d[i],i,this_file.getName(),this_file.getDate()))
  414. #~ self.files.update({this_file.getName(),this_file})
  415. self.files = self.files + [this_file]
  416. if this_file.getSize() == FileObj.FILE_IS_DIR:
  417. item = SortableTableWidgetItem(d[i],' '+d[i], this_file) # directories sort first
  418. else:
  419. item = SortableTableWidgetItem(d[i],d[i], this_file)
  420. item.setFlags(Qt.ItemIsEnabled)
  421. #item.setData(32,this_file) # keep a hidden copy of the file object
  422. item.setToolTip(this_file.getName())
  423. self.lb.setItem(i,0,item)
  424. #lb.itemAt(i,0).setFlags(Qt.ItemIsEnabled) #NoItemFlags)
  425. # get the comment from database & xattrs, either can fail
  426. comment = ""
  427. row = self.db.getData(this_file.getName())
  428. if row and len(row)>0:
  429. comment = row[3]
  430. xattrComment = this_file.getXattrComment()
  431. self.lb.setItem(i,3,QTableWidgetItem(comment))
  432. if xattrComment != comment:
  433. self.lb.item(i,3).setBackground(QBrush(Qt.yellow))
  434. print("got two comments <{}> and <{}>".format(comment, xattrComment))
  435. dt = this_file.getDate()
  436. da = SortableTableWidgetItem(dnDataBase.getShortDate(dt),dt,this_file)
  437. #da.setFont(small_font)
  438. da.setToolTip(dt)
  439. da.setFlags(Qt.ItemIsEnabled)
  440. self.lb.setItem(i,1,da)
  441. si = this_file.getSize()
  442. if si==FileObj.FILE_IS_DIR:
  443. sa = SortableTableWidgetItem('',0,this_file)
  444. item.setIcon(dirIcon)
  445. elif si==FileObj.FILE_IS_LINK:
  446. sa = SortableTableWidgetItem('symlink',-1,this_file)
  447. item.setIcon(linkIcon)
  448. dst = os.path.realpath(this_file.getName())
  449. sa.setToolTip("symlink: " + dst)
  450. else:
  451. sa = SortableTableWidgetItem(str(si),si,this_file)
  452. item.setIcon(fileIcon)
  453. sa.setTextAlignment(Qt.AlignRight)
  454. sa.setFlags(Qt.ItemIsEnabled)
  455. self.lb.setItem(i,2,sa)
  456. self.refilling = False
  457. self.lb.sortingEnabled = True
  458. self.lb.resizeColumnToContents(1)
  459. def change(self,x):
  460. if self.refilling:
  461. return
  462. print("debugging " + x.text() + " r:" + str(x.row()) + " c:" + str(x.column()))
  463. print(" selected file: "+self.lb.item(x.row(),0).file_object.getName())
  464. the_file = self.lb.item(x.row(),0).file_object
  465. print(" and the row file is "+the_file.getName())
  466. r = the_file.setXattrComment(str(x.text())) # TODO: change priority
  467. if r:
  468. self.db.setData(the_file.getName(),x.text())
  469. def restore_from_database(self):
  470. print("restore from database")
  471. # retrieve the full path name
  472. fileName = str(self.lb.item(self.lb.currentRow(),0).file_object.getName())
  473. print("using filename: "+fileName)
  474. existing_comment = str(self.lb.item(self.lb.currentRow(),3).text())
  475. print("restore....existing="+existing_comment+"=")
  476. if len(existing_comment) > 0:
  477. m = QMessageBox()
  478. m.setText("This file already has a comment. Overwrite?")
  479. m.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel);
  480. if m.exec_() != QMessageBox.Ok:
  481. return
  482. fo_row = self.db.getData(fileName)
  483. if fo_row and len(fo_row)>1:
  484. comment = fo_row[3]
  485. print(fileName,fo_row[0],comment)
  486. the_file = dn.files[self.lb.currentRow()]
  487. the_file.setComment(comment)
  488. self.lb.setItem(self.lb.currentRow(),3,QTableWidgetItem(comment))
  489. if __name__=="__main__":
  490. p = parse()
  491. if len(p.dirname)>1 and p.dirname[-1]=='/':
  492. p.dirname = p.dirname[:-1]
  493. if os.path.isdir(p.dirname):
  494. p.dirname = p.dirname + '/'
  495. print(p.dirname)
  496. db = dnDataBase()
  497. # TODO: if there's a file specified, jump to it
  498. a = QApplication([])
  499. dn = DirNotes(p.dirname,db)
  500. dn.show()
  501. a.exec_()
  502. #xattr.setxattr(filename,COMMENT_KEY,commentText)
  503. ''' files from directories
  504. use os.isfile()
  505. os.isdir()
  506. current, dirs, files = os.walk("path").next()
  507. possible set folllowLinks=True'''
  508. ''' notes from the wdrm project
  509. table showed
  510. filename, size, date size, date, desc
  511. at start, fills the list of all the files
  512. skip the . entry
  513. '''
  514. ''' should we also do user.xdg.tags="TagA,TagB" ?
  515. user.charset
  516. user.creator=application_name or user.xdg.creator
  517. user.xdg.origin.url
  518. user.xdg.language=[RFC3066/ISO639]
  519. user.xdg.publisher
  520. '''
  521. ''' TODO: add cut-copy-paste for comments '''
  522. ''' TODO: also need a way to display-&-restore comments from the database '''
  523. ''' TODO: implement startup -s and -m for size and date '''
  524. ''' TODO: add an icon for the app '''
  525. ''' TODO: create 'show comment history' popup '''
  526. ''' TODO: add dual-pane for file-move, file-copy '''
  527. ''' commandline xattr
  528. getfattr -h (don't follow symlink) -d (dump all properties)
  529. '''
  530. ''' if the args line contains a file, jump to it '''