Browse Source

add dncli to the family

Pat Beirne 1 year ago
parent
commit
0833acd067
3 changed files with 182 additions and 0 deletions
  1. 36 0
      c_dirnotes.py
  2. 43 0
      dirnotes
  3. 103 0
      dncli

+ 36 - 0
c_dirnotes.py

@@ -96,6 +96,39 @@ COLOR_THEME = ''' { "heading": ("yellow","blue"),
 now = time.time()
 YEAR = 3600*24*365
 
+#
+# deal with the odd aspects of MacOS
+#
+#   xattr is loaded as a separate library, with different argument names
+#   to set/get the comments in Finder, we need to call osascript
+#
+
+if sys.platform == "darwin":
+  # we get xattr from PyPi
+  try:
+    import xattr
+  except:
+    print("This program requires the 'xattr' package. Please use 'pip3 install xattr' to install the package")
+    sys.exit(1)
+  def do_setxattr(file_name, attr_name, attr_content):
+    if attr_name.endswith("omment"):
+      p = Popen(['osascript','-'] + f'tell application "Finder" to set comment of (POSIX file "{file_name}" as alias) to "{attr_content}"')
+      p.communicate()
+    else:
+      xattr.setxattr(file_name, attr_name, plistlib.dumps(attr_content))
+
+  os.setxattr = do_setxattr
+  #os.setxattr = xattr.setxattr
+  def do_getxattr(file_name, attr_name):
+    attr_data = xattr.getxattr(file_name, attr_name)
+    if attr_name.endswith("omment"):
+      return plistlib.loads(attr_data)
+    else:
+      return attr_data
+  os.getxattr = xattr.getxattr
+else:
+  pass
+
 class Pane:
   ''' holds the whole display: handles file list directly,
       fills a child pad with the file info,
@@ -919,3 +952,6 @@ cwd = args.directory
 curses.wrapper(main, cwd)
 
 # dirnotes database is name, date, size, comment, comment_date, author
+
+# symlinks: follow_symlinks should always be True, because symlinks in Linux
+#    can't have xattr....it appears to be the same in darwin

+ 43 - 0
dirnotes

@@ -218,6 +218,47 @@ class HelpWidget(QDialog):
 		self.pb.pressed.connect(self.close)
 		self.show()
 
+def loadWindowIcon():  # TODO: change border/folder to dark grey, no shadow, smaller
+  icon = ["32 32 6 1",
+"   c None",
+".  c #666666",
+"+  c #FFFFFF",
+"@  c #848484",
+"#  c #000000",
+"$  c #FCE883",
+"                                ",
+"  ........                      ",
+" .++++++++.                     ",
+" .+++++++++..................   ",
+" .+++++++++++++++++++++++++++.  ",
+" .+++++++++++++++++++++++++++.  ",
+" .++..+......++@@@@@@@@@@@@@@@@@",
+" .++..++++++++#################@",
+" .+++++++++++#$$$$$$$$$$$$$$$$$#",
+" .++..+.....+#$$$$$$$$$$$$$$$$$#",
+" .++..+++++++#$$$$$$$$$$$$$$$$$#",
+" .+++++++++++#$$#############$$#",
+" .++..+.....+#$$$$$$$$$$$$$$$$$#",
+" .++..+++++++#$$########$$$$$$$#",
+" .+++++++++++#$$$$$$$$$$$$$$$$$#",
+" .++..+.....+#$$$$$$$$$$$$$$$$$#",
+" .++..++++++++#######$$$####### ",
+" .++++++++++++++++++#$$#++++++  ",
+" .++..+............+#$#++++++.  ",
+" .++..++++++++++++++##+++++++.  ",
+" .++++++++++++++++++#++++++++.  ",
+" .++..+............++++++++++.  ",
+" .++..+++++++++++++++++++++++.  ",
+" .+++++++++++++++++++++++++++.  ",
+" .++..+................++++++.  ",
+" .++..+++++++++++++++++++++++.  ",
+" .+++++++++++++++++++++++++++.  ",
+" .++..+................++++++.  ",
+" .++..+++++++++++++++++++++++.  ",
+" .+++++++++++++++++++++++++++.  ",
+"  ...........................   ",
+"                                "]
+  return QPixmap(icon)
 
 ''' a widget that shows only a dir listing
  '''
@@ -364,6 +405,8 @@ class DirNotes(QMainWindow):
 		#~ QShortcut(QKeySequence("Ctrl+Q"), self, self.close)	
 		self.setWindowTitle("DirNotes   Alt-F for menu  Dir: "+self.curPath)
 		self.setMinimumSize(600,700)
+		wi = loadWindowIcon()
+		self.setWindowIcon(QIcon(wi))
 		lb.setFocus()
 	def closeEvent(self,e):
 		print("closing")

+ 103 - 0
dncli

@@ -0,0 +1,103 @@
+#!/usr/bin/python3
+# TODO: change the default file list to only apply to listing, not copying/erasing/creating/appending/zapping
+
+VERSION = "0.1"
+
+import os, sys, argparse
+
+def file_copy(f,target,target_is_dir,force):
+  print(f"call file_copy with args={target},{target_is_dir} and {force}")
+  dest = target if not target_is_dir else target+'/'+os.path.basename(f)
+  if os.path.exists(dest) and not force:
+    go = input("The copy target <<" + dest + ">> exists. Overwrite? (y or n) ")
+    if go != 'y' and go != 'Y':
+      return
+  print(f"copy from {f} to {dest}")
+
+def file_zap(f,all_flag):
+  print(f"zapping the comment history of {f}")
+  if all_flag:
+    print("zapping the entire database")
+
+def file_erase(f, extra):
+  print(f"erase the comment on file {f}")
+
+
+def main(args):
+  parser = argparse.ArgumentParser(description="Display or add comments to files",
+    epilog="Some options conflict. Use only one of: -l -c -a -H -e -z -Z and one of -d -x")
+  parser.add_argument('-V',"--version", action="version",version=f"dncli ver:{VERSION}")
+  parser.add_argument('-v',"--verbose", action='store_true',help="verbose, almost debugging; do not use in scripts")
+  parser.add_argument('-j',"--json",    action="store_true",help="output in JSON format")
+  parser.add_argument('-l',"--listall", action="store_true",help="list all files, including those without comments")
+  parser.add_argument('-d',"--db",      action="store_true",help="list comments from database")
+  parser.add_argument('-x',"--xattr",   action="store_true",help="list comments from xattr")
+  parser.add_argument('-n',"--minimal", action="store_true",help="output only comments; useful in scripting")
+  parser.add_argument('-H',"--history", action="store_true",help="output the history of comments for a file")
+  parser.add_argument('-c',"--create",  metavar="comment",  help="add a comment to a file")
+  parser.add_argument('-a',"--append",  metavar="comment",  help="append to a comment on a file, separator=';'")
+  parser.add_argument('-C',"--copy",    action="store_true",help="copy a file with its comments")
+  parser.add_argument('-y',"--cp_force",action="store_true",help="copy over existing files")
+  parser.add_argument('-e',"--erase",   action="store_true",help="erase the comment on a file")
+  parser.add_argument('-z',"--zap",     action="store_true",help="clear the comment history on a file")
+  parser.add_argument('-Z',"--zapall",   action="store_true",help="clear the comment history in the entire database")
+  parser.add_argument('file_list',nargs='*',help="file(s) for comments; default is all files in ./")
+  args = parser.parse_args()
+
+  # default is to display all files that have comments
+
+  # major modes are: display (-l -H), add-comment (-a -c -e), clear-history(-z -Z), copy (-C)
+  # determine the major mode, then apply an appropriate function over the file_list
+
+  #====== 1) build the file list =============
+
+  files = args.file_list
+  if not files:
+    files = os.listdir(".")
+  # if the user gave me a single dir...expand it
+  elif len(files)==1 and os.path.isdir(files[0]):
+    files = os.listdir(files[0])
+  print("got the files:", files)
+
+  #======= 2) build the function
+  if args.create or args.append:
+    print(f"create/append: {args.create} . {args.append}")
+  elif args.erase:
+    print(f"erase command")
+    func = file_erase
+    loop_args = (0,)
+  elif args.zap or args.zapall:
+    print(f"got a zap command {args.zap} . {args.zapall}")
+    func = file_zap
+    loop_args = (args.zapall,)
+    if args.zapall:
+      files = (1,)
+  elif args.copy:
+    print(f"got a copy command to {args.copy}")
+    # the last item on the file list is the target
+    n_files = len(files)
+    if n_files < 2:
+      print(f"the copy command requires at least two arguments, the last one is the destination")
+      sys.exit(1)
+    target = files[-1]
+    target_is_directory = os.path.isdir(target)
+    files = files[:-1]
+    print(f"copy from {files} to {target}")
+    if n_files > 2 and not target_is_directory:
+      print(f"multiple copy files must go to a target directory")
+      sys.exit(3)
+    func = file_copy
+    loop_args = (target, target_is_directory, args.cp_force)
+  else:
+    print(f"display command with option: {args.listall}")
+
+
+  #====== 3) loop on the list, execute the function =============
+  for f in files:
+    func(f,*loop_args)
+
+
+if __name__ == "__main__":
+  main(sys.argv)
+
+