|
@@ -1,4 +1,4 @@
|
|
-#!/usr/bin/python3
|
|
|
|
|
|
+#!/usr/bin/env python3
|
|
|
|
|
|
# TOTO: when changing directories, keep the sort order?
|
|
# TOTO: when changing directories, keep the sort order?
|
|
# TODO: fix the 'reload' function....need 'this_dir' in Files class
|
|
# TODO: fix the 'reload' function....need 'this_dir' in Files class
|
|
@@ -21,23 +21,22 @@ import os, time, stat, sys, shutil
|
|
|
|
|
|
import time, math
|
|
import time, math
|
|
import curses, sqlite3, curses.textpad
|
|
import curses, sqlite3, curses.textpad
|
|
-import logging, getpass
|
|
|
|
|
|
+import logging, getpass, argparse
|
|
|
|
+import json
|
|
|
|
|
|
-VERSION = "1.4"
|
|
|
|
|
|
+VERSION = "1.5"
|
|
# these may be different on MacOS
|
|
# these may be different on MacOS
|
|
-XATTR_COMMENT = "user.xdg.comment"
|
|
|
|
-XATTR_AUTHOR = "user.xdg.comment.author"
|
|
|
|
-XATTR_DATE = "user.xdg.comment.date"
|
|
|
|
-COMMENT_OWNER = os.getlogin()
|
|
|
|
|
|
+xattr_comment = "user.xdg.comment"
|
|
|
|
+xattr_author = "user.xdg.comment.author"
|
|
|
|
+xattr_date = "user.xdg.comment.date"
|
|
|
|
+DEFAULT_CONFIG_FILE = "~/.dirnotes.conf"
|
|
|
|
|
|
# convert the ~/ form to a fully qualified path
|
|
# convert the ~/ form to a fully qualified path
|
|
-DATABASE_NAME = "~/.dirnotes.db"
|
|
|
|
-DATABASE_NAME = os.path.expanduser(DATABASE_NAME) # doesn't deref symlinks
|
|
|
|
|
|
+# database_name = "~/.dirnotes.db"
|
|
|
|
|
|
-MODE_DATABASE = 0
|
|
|
|
-MODE_XATTR = 1
|
|
|
|
-mode_names = ["<Database mode> ","<Xattr mode>"]
|
|
|
|
-mode = MODE_XATTR
|
|
|
|
|
|
+mode_names = {"db":"<Database mode> ","xattr":"<Xattr mode>"}
|
|
|
|
+modes = ("db","xattr")
|
|
|
|
+mode = "db"
|
|
|
|
|
|
### commands
|
|
### commands
|
|
CMD_COPY = ord('c') # open dialog to copy-with-comment
|
|
CMD_COPY = ord('c') # open dialog to copy-with-comment
|
|
@@ -69,6 +68,16 @@ CMD_CD = ord('\n')
|
|
# file comments will ALWAYS be written to both xattrs & database
|
|
# file comments will ALWAYS be written to both xattrs & database
|
|
# access failure is shown once per directory
|
|
# access failure is shown once per directory
|
|
|
|
|
|
|
|
+# 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")
|
|
|
|
+}
|
|
|
|
+
|
|
### colors
|
|
### colors
|
|
|
|
|
|
CP_TITLE = 1
|
|
CP_TITLE = 1
|
|
@@ -189,7 +198,7 @@ class Pane:
|
|
|
|
|
|
dbComment = f.getDbComment()
|
|
dbComment = f.getDbComment()
|
|
xattrComment = f.getXattrComment()
|
|
xattrComment = f.getXattrComment()
|
|
- comment = xattrComment if mode==MODE_XATTR else dbComment
|
|
|
|
|
|
+ comment = xattrComment if mode=="xattr" else dbComment
|
|
if dbComment != xattrComment:
|
|
if dbComment != xattrComment:
|
|
self.some_comments_differ = True
|
|
self.some_comments_differ = True
|
|
self.file_pad.attron(COLOR_HELP)
|
|
self.file_pad.attron(COLOR_HELP)
|
|
@@ -279,7 +288,7 @@ class Files():
|
|
|
|
|
|
self.db = None
|
|
self.db = None
|
|
try:
|
|
try:
|
|
- self.db = sqlite3.connect(DATABASE_NAME)
|
|
|
|
|
|
+ self.db = sqlite3.connect(database_name)
|
|
c = self.db.cursor()
|
|
c = self.db.cursor()
|
|
c.execute("select * from dirnotes")
|
|
c.execute("select * from dirnotes")
|
|
except sqlite3.OperationalError:
|
|
except sqlite3.OperationalError:
|
|
@@ -296,8 +305,12 @@ class Files():
|
|
|
|
|
|
|
|
|
|
def sortName(a):
|
|
def sortName(a):
|
|
|
|
+ ''' when sorting by name put the .. and other <dir> entries first '''
|
|
if a.getFileName() == '..':
|
|
if a.getFileName() == '..':
|
|
return "\x00"
|
|
return "\x00"
|
|
|
|
+ if a.isDir():
|
|
|
|
+ return ' ' + a.getFileName()
|
|
|
|
+ # else:
|
|
return a.getFileName()
|
|
return a.getFileName()
|
|
|
|
|
|
def sortDate(a):
|
|
def sortDate(a):
|
|
@@ -310,15 +323,18 @@ class Files():
|
|
return 0
|
|
return 0
|
|
return a.getSize()
|
|
return a.getSize()
|
|
|
|
|
|
- def getCurDir(self):
|
|
|
|
- return self.directory
|
|
|
|
- def getMasterComment(self):
|
|
|
|
- return self.directory.xattrComment if mode==MODE_XATTR else self.directory.dbComment
|
|
|
|
|
|
+ def sortComment(a):
|
|
|
|
+ return a.getComment()
|
|
|
|
|
|
sort_mode = sortName
|
|
sort_mode = sortName
|
|
def sort(self):
|
|
def sort(self):
|
|
self.files.sort(key = Files.sort_mode)
|
|
self.files.sort(key = Files.sort_mode)
|
|
|
|
|
|
|
|
+ def getCurDir(self):
|
|
|
|
+ return self.directory
|
|
|
|
+ def getMasterComment(self):
|
|
|
|
+ return self.directory.getComment()
|
|
|
|
+
|
|
## accessors ##
|
|
## accessors ##
|
|
def __len__(self):
|
|
def __len__(self):
|
|
return len(self.files)
|
|
return len(self.files)
|
|
@@ -361,9 +377,9 @@ class FileObj():
|
|
self.dbDate = None
|
|
self.dbDate = None
|
|
self.commentsDiffer = False
|
|
self.commentsDiffer = False
|
|
try:
|
|
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 = float(os.getxattr(fileName, XATTR_DATE, follow_symlinks=False).decode())
|
|
|
|
|
|
+ self.xattrComment = os.getxattr(fileName, xattr_comment, follow_symlinks=False).decode()
|
|
|
|
+ self.xattrAuthor = os.getxattr(fileName, xattr_author, follow_symlinks=False).decode()
|
|
|
|
+ self.xattrDate = float(os.getxattr(fileName, xattr_date, follow_symlinks=False).decode())
|
|
self.commentsDiffer = True if self.xattrComment == self.dbComment else False
|
|
self.commentsDiffer = True if self.xattrComment == self.dbComment else False
|
|
except: # no xattr comment
|
|
except: # no xattr comment
|
|
pass
|
|
pass
|
|
@@ -389,9 +405,9 @@ class FileObj():
|
|
return self.dbDate
|
|
return self.dbDate
|
|
def setDbComment(self,newComment):
|
|
def setDbComment(self,newComment):
|
|
try:
|
|
try:
|
|
- self.db = sqlite3.connect(DATABASE_NAME)
|
|
|
|
|
|
+ self.db = sqlite3.connect(database_name)
|
|
except sqlite3.OperationalError:
|
|
except sqlite3.OperationalError:
|
|
- logging.info(f"database {DATABASE_NAME} not found")
|
|
|
|
|
|
+ logging.info(f"database {database_name} not found")
|
|
raise OperationalError
|
|
raise OperationalError
|
|
c = self.db.cursor()
|
|
c = self.db.cursor()
|
|
s = os.lstat(self.fileName)
|
|
s = os.lstat(self.fileName)
|
|
@@ -417,9 +433,9 @@ class FileObj():
|
|
def setXattrComment(self,newComment):
|
|
def setXattrComment(self,newComment):
|
|
logging.info(f"set comment {newComment} on file {self.fileName}")
|
|
logging.info(f"set comment {newComment} on file {self.fileName}")
|
|
try:
|
|
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(str(time.time()),'utf8'),follow_symlinks=False)
|
|
|
|
|
|
+ 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(str(time.time()),'utf8'),follow_symlinks=False)
|
|
self.xattrAuthor = getpass.getuser()
|
|
self.xattrAuthor = getpass.getuser()
|
|
self.xattrDate = time.time() # alternatively, re-instantiate this FileObj
|
|
self.xattrDate = time.time() # alternatively, re-instantiate this FileObj
|
|
self.xattrComment = newComment
|
|
self.xattrComment = newComment
|
|
@@ -440,8 +456,12 @@ class FileObj():
|
|
errorBox("is this a VFAT or EXFAT volume? these don't allow comments")
|
|
errorBox("is this a VFAT or EXFAT volume? these don't allow comments")
|
|
return False
|
|
return 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):
|
|
def getDate(self):
|
|
- return self.date
|
|
|
|
|
|
+ return self.date
|
|
def getSize(self):
|
|
def getSize(self):
|
|
return self.size
|
|
return self.size
|
|
def isDir(self):
|
|
def isDir(self):
|
|
@@ -596,17 +616,18 @@ def show_help2():
|
|
del w
|
|
del w
|
|
|
|
|
|
#TODO: fix this to paint_dialog
|
|
#TODO: fix this to paint_dialog
|
|
|
|
+#TODO: fix to allow upper/lower case responses
|
|
sort_string = """
|
|
sort_string = """
|
|
Select sort order:
|
|
Select sort order:
|
|
|
|
|
|
- Name
|
|
|
|
- Date
|
|
|
|
- Size
|
|
|
|
- Comment"""
|
|
|
|
|
|
+ name
|
|
|
|
+ date
|
|
|
|
+ size
|
|
|
|
+ comment"""
|
|
def show_sort():
|
|
def show_sort():
|
|
h = paint_dialog(COLOR_HELP,sort_string)
|
|
h = paint_dialog(COLOR_HELP,sort_string)
|
|
h.attron(COLOR_TITLE)
|
|
h.attron(COLOR_TITLE)
|
|
- h.addstr(3,3,"N") or h.addstr(4,3,"D") or h.addstr(5,3,"S") or h.addstr(6,3,"C")
|
|
|
|
|
|
+ h.addstr(3,3,"n") or h.addstr(4,3,"d") or h.addstr(5,3,"s") or h.addstr(6,3,"c")
|
|
h.attroff(COLOR_TITLE)
|
|
h.attroff(COLOR_TITLE)
|
|
h.refresh()
|
|
h.refresh()
|
|
c = h.getch()
|
|
c = h.getch()
|
|
@@ -622,7 +643,7 @@ Comments detail:
|
|
def show_detail(f):
|
|
def show_detail(f):
|
|
global mode
|
|
global mode
|
|
h = paint_dialog(COLOR_HELP,detail_string)
|
|
h = paint_dialog(COLOR_HELP,detail_string)
|
|
- if mode==MODE_XATTR:
|
|
|
|
|
|
+ if mode=="xattr":
|
|
h.addstr(1,20,"from xattrs")
|
|
h.addstr(1,20,"from xattrs")
|
|
c = f.getXattrComment()
|
|
c = f.getXattrComment()
|
|
a = f.getXattrAuthor()
|
|
a = f.getXattrAuthor()
|
|
@@ -652,11 +673,10 @@ def edit_fn(c):
|
|
return 7
|
|
return 7
|
|
return c
|
|
return c
|
|
|
|
|
|
-def main(w):
|
|
|
|
|
|
+def main(w, cwd):
|
|
global files, edit_done, mode
|
|
global files, edit_done, mode
|
|
global COLOR_TITLE, COLOR_BODY, COLOR_FOCUS, COLOR_ERROR, COLOR_HELP
|
|
global COLOR_TITLE, COLOR_BODY, COLOR_FOCUS, COLOR_ERROR, COLOR_HELP
|
|
- logging.basicConfig(filename='/tmp/dirnotes.log', level=logging.DEBUG)
|
|
|
|
- logging.info("starting curses dirnotes")
|
|
|
|
|
|
+
|
|
curses.init_pair(CP_TITLE, curses.COLOR_YELLOW,curses.COLOR_BLUE)
|
|
curses.init_pair(CP_TITLE, curses.COLOR_YELLOW,curses.COLOR_BLUE)
|
|
curses.init_pair(CP_BODY, curses.COLOR_WHITE,curses.COLOR_BLUE)
|
|
curses.init_pair(CP_BODY, curses.COLOR_WHITE,curses.COLOR_BLUE)
|
|
curses.init_pair(CP_FOCUS, curses.COLOR_BLACK,curses.COLOR_CYAN)
|
|
curses.init_pair(CP_FOCUS, curses.COLOR_BLACK,curses.COLOR_CYAN)
|
|
@@ -672,10 +692,6 @@ def main(w):
|
|
COLOR_DIFFER = curses.color_pair(CP_DIFFER)
|
|
COLOR_DIFFER = curses.color_pair(CP_DIFFER)
|
|
logging.info(f"COLOR_DIFFER is {COLOR_DIFFER}")
|
|
logging.info(f"COLOR_DIFFER is {COLOR_DIFFER}")
|
|
|
|
|
|
- if len(sys.argv) > 1:
|
|
|
|
- cwd = sys.argv[1]
|
|
|
|
- else:
|
|
|
|
- cwd = os.getcwd()
|
|
|
|
files = Files(cwd)
|
|
files = Files(cwd)
|
|
logging.info(f"got files, len={len(files)}")
|
|
logging.info(f"got files, len={len(files)}")
|
|
|
|
|
|
@@ -695,12 +711,14 @@ def main(w):
|
|
|
|
|
|
elif c == CMD_SORT:
|
|
elif c == CMD_SORT:
|
|
c = show_sort()
|
|
c = show_sort()
|
|
- if c == ord('s'):
|
|
|
|
|
|
+ if c == ord('s') or c == ord('S'):
|
|
Files.sort_mode = Files.sortSize
|
|
Files.sort_mode = Files.sortSize
|
|
- elif c == ord('n'):
|
|
|
|
|
|
+ elif c == ord('n') or c == ord('N'):
|
|
Files.sort_mode = Files.sortName
|
|
Files.sort_mode = Files.sortName
|
|
- elif c == ord('d'):
|
|
|
|
|
|
+ elif c == ord('d') or c == ord('D'):
|
|
Files.sort_mode = Files.sortDate
|
|
Files.sort_mode = Files.sortDate
|
|
|
|
+ elif c == ord('c') or c == ord('C'):
|
|
|
|
+ Files.sort_mode = Files.sortComment
|
|
files.sort()
|
|
files.sort()
|
|
mywin.refill()
|
|
mywin.refill()
|
|
mywin.refresh()
|
|
mywin.refresh()
|
|
@@ -723,7 +741,7 @@ def main(w):
|
|
mywin.refresh()
|
|
mywin.refresh()
|
|
|
|
|
|
elif c == CMD_MODE:
|
|
elif c == CMD_MODE:
|
|
- mode = MODE_DATABASE if mode==MODE_XATTR else MODE_XATTR
|
|
|
|
|
|
+ mode = "db" if mode=="xattr" else "xattr"
|
|
mywin.refill()
|
|
mywin.refill()
|
|
mywin.refresh()
|
|
mywin.refresh()
|
|
|
|
|
|
@@ -778,7 +796,7 @@ def main(w):
|
|
cp_cmnt_ask = curses.newwin(6,40,5,5)
|
|
cp_cmnt_ask = curses.newwin(6,40,5,5)
|
|
cp_cmnt_ask.border()
|
|
cp_cmnt_ask.border()
|
|
cp_cmnt_ask.addstr(1,1,"Copy comments to ==> ")
|
|
cp_cmnt_ask.addstr(1,1,"Copy comments to ==> ")
|
|
- cp_cmnt_ask.addstr(1,22,"database" if mode==MODE_XATTR else "xattr")
|
|
|
|
|
|
+ cp_cmnt_ask.addstr(1,22,"database" if mode=="xattr" else "xattr")
|
|
cp_cmnt_ask.addstr(2,1,"1 just this file")
|
|
cp_cmnt_ask.addstr(2,1,"1 just this file")
|
|
cp_cmnt_ask.addstr(3,1,"a all files with comments")
|
|
cp_cmnt_ask.addstr(3,1,"a all files with comments")
|
|
cp_cmnt_ask.addstr(4,1,"esc to cancel")
|
|
cp_cmnt_ask.addstr(4,1,"esc to cancel")
|
|
@@ -794,7 +812,7 @@ def main(w):
|
|
else:
|
|
else:
|
|
collection = files
|
|
collection = files
|
|
for f in collection:
|
|
for f in collection:
|
|
- if mode==MODE_XATTR:
|
|
|
|
|
|
+ if mode=="xattr":
|
|
if f.getXattrComment():
|
|
if f.getXattrComment():
|
|
f.setDbComment(f.getXattrComment())
|
|
f.setDbComment(f.getXattrComment())
|
|
else:
|
|
else:
|
|
@@ -846,6 +864,58 @@ def main(w):
|
|
mywin.resize()
|
|
mywin.resize()
|
|
#mywin.refresh()
|
|
#mywin.refresh()
|
|
|
|
|
|
-curses.wrapper(main)
|
|
|
|
|
|
+def pre_main():
|
|
|
|
+ # done before we switch to curses mode
|
|
|
|
+ logging.basicConfig(filename='/tmp/dirnotes.log', level=logging.DEBUG)
|
|
|
|
+ logging.info("starting curses dirnotes")
|
|
|
|
+
|
|
|
|
+ parser = argparse.ArgumentParser(description="Add comments to files")
|
|
|
|
+ parser.add_argument('-c','--config', dest='config_file', help="config file (json format)")
|
|
|
|
+ parser.add_argument('-v','--version', action='version', version=f"dirnotes ver:{VERSION}")
|
|
|
|
+ parser.add_argument('directory', type=str, default='.', nargs='?', help="directory to start")
|
|
|
|
+ args = parser.parse_args()
|
|
|
|
+ logging.info(args)
|
|
|
|
+
|
|
|
|
+ if args.config_file:
|
|
|
|
+ config_file = args.config_file
|
|
|
|
+ else:
|
|
|
|
+ config_file = 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 the config file {config_file}")
|
|
|
|
+ printf("please check the .json syntax")
|
|
|
|
+ time.sleep(2)
|
|
|
|
+ except FileNotFoundError:
|
|
|
|
+ print(f"config file {config_file} not found, using default settings & creating a default")
|
|
|
|
+ try:
|
|
|
|
+ with open(config_file,"w") as f:
|
|
|
|
+ json.dump(config,f,indent=4)
|
|
|
|
+ except:
|
|
|
|
+ print(f"problem creating file {config_file}")
|
|
|
|
+ time.sleep(2)
|
|
|
|
+
|
|
|
|
+ # print(repr(config))
|
|
|
|
+ # print("start_mode",config["start_mode"])
|
|
|
|
+
|
|
|
|
+ # change ~ tilde to username
|
|
|
|
+ config["database"] = os.path.expanduser(config["database"])
|
|
|
|
+ # load globals from the config
|
|
|
|
+ return args, config
|
|
|
|
+
|
|
|
|
+args, config = pre_main()
|
|
|
|
+
|
|
|
|
+mode = config["start_mode"]
|
|
|
|
+xattr_comment = config["xattr_tag"]
|
|
|
|
+xattr_author = config["xattr_tag"] + ".author"
|
|
|
|
+xattr_date = config["xattr_tag"] + ".date"
|
|
|
|
+database_name = os.path.expanduser(config["database"])
|
|
|
|
+cwd = args.directory
|
|
|
|
+
|
|
|
|
+curses.wrapper(main, cwd)
|
|
|
|
|
|
# dirnotes database is name, date, size, comment, comment_date, author
|
|
# dirnotes database is name, date, size, comment, comment_date, author
|