2 Commits 80e41b5324 ... f7dc54cd2f

Author SHA1 Message Date
  Pat Beirne f7dc54cd2f after a merge 2 years ago
  Pat Beirne 67214ba444 so many changes 2 years ago
4 changed files with 826 additions and 665 deletions
  1. 29 12
      README.md
  2. 626 483
      dirnotes
  3. 170 170
      dirnotes-cli
  4. 1 0
      dirnotes-cli.md

+ 29 - 12
README.md

@@ -10,7 +10,7 @@ Table of Contents
   * [LIMITATIONS](#limitations)
   * [LIMITATIONS](#limitations)
   * [PROGRAMMER NOTES](#programmer-notes)
   * [PROGRAMMER NOTES](#programmer-notes)
     * [MacOS](#macos)
     * [MacOS](#macos)
-  * [DEVELOPMENT STATUS](#development-status)
+  * [DEVELOPMENT STATUS](#status)
 
 
 ## SYNOPSIS
 ## SYNOPSIS
 
 
@@ -40,6 +40,8 @@ _s_ for sort, _M_ to change between xattr/database priority.
 
 
 The **<code>dirnotes-cli</code>** has options for _-l_ list and _-c_ create a comment. 
 The **<code>dirnotes-cli</code>** has options for _-l_ list and _-c_ create a comment. 
 
 
+All three apps in the **dirnotes** family have the ability to copy files from the current directory. 
+ 
 ## INSTALLATION
 ## INSTALLATION
 
 
 Each of the 3 apps in the family is self contained. 
 Each of the 3 apps in the family is self contained. 
@@ -55,6 +57,7 @@ This is a JSON file, with three attributes that are important:
 > * database (default: <code>~/.dirnotes.db</code>) 
 > * database (default: <code>~/.dirnotes.db</code>) 
 > * start_mode (_xattr_ or _db_ priority)
 > * start_mode (_xattr_ or _db_ priority)
 
 
+The _config_file_ should be auto-generated the first time one of the **dirnotes** apps is run.
 [_not fully implemented_]
 [_not fully implemented_]
 
 
 ## LIMITATIONS 
 ## LIMITATIONS 
@@ -62,12 +65,13 @@ This is a JSON file, with three attributes that are important:
 
 
 The file comments are located in two locations: a database, and in the 
 The file comments are located in two locations: a database, and in the 
 xattr properties of the file. Each of these storage locations has its 
 xattr properties of the file. Each of these storage locations has its 
-own benefits and limitations:
+own benefits and limitations. These can be summed up: **_xattr_** comments
+follow the iNode, **_database_** comments follow the file name.
 
 
 ### xattr
 ### xattr
 
 
 Comments stored in the xattr properties can be copied/moved with the file, if you
 Comments stored in the xattr properties can be copied/moved with the file, if you
-use the correct options for <code>**cp**</code>. The <code>**mv**</code> utility 
+use the correct options: <code>**cp -p**</code>. The <code>**mv**</code> utility 
 automatically preserves _xattr_. Other programs can also be coerced into 
 automatically preserves _xattr_. Other programs can also be coerced into 
 perserving _xattr_ properties:
 perserving _xattr_ properties:
 
 
@@ -136,7 +140,7 @@ The comments are stored in an Sqlite3 database, usually located at "~/.dirnotes.
 |author     |   the system name of the user who created the comment  |  patb | 
 |author     |   the system name of the user who created the comment  |  patb | 
 
 
 
 
-The 'date' and 'size' fields reflect the file's modification date and size at the time of the last edit of the file comment, which is also 'comment_date'.
+The _date_ and _size_ fields reflect the file's modification date and size at the time of the last edit of the file comment, which is stored in _comment_date_.
 
 
 As comments are editted or appended, new records are added to the database. Older records are are not purged. This gives you a history of the comments, but it means that fetching the most recent comment involves something like
 As comments are editted or appended, new records are added to the database. Older records are are not purged. This gives you a history of the comments, but it means that fetching the most recent comment involves something like
 
 
@@ -146,7 +150,15 @@ As comments are editted or appended, new records are added to the database. Olde
 
 
 and just fetch the first record.
 and just fetch the first record.
 
 
-### MacOS
+The database is created the first time one of the **dirnotes** apps is run.
+
+  * misc
+
+The <code>**dirnotes**</code> gui app has a desktop icon built into the code. There is not need for an external .icon file.
+
+There was _no_ consideration given for language translation. Email [me](mail:patb@pbeirne.com) if you want this, or can help.
+
+### MacOS {#macos}
 
 
 The **MacOS** inherently supports file comments. The Finder app manages most of the user activity. It handles file comments in a similar manner to **Dirnotes**. Comments are stored in two places:
 The **MacOS** inherently supports file comments. The Finder app manages most of the user activity. It handles file comments in a similar manner to **Dirnotes**. Comments are stored in two places:
 
 
@@ -162,14 +174,19 @@ If the Finder is used to copy/move files, the comments are moved properly to bot
 
 
 **MacOS** has AppleScript, by which you can ask the Finder to perform the file copy/move. In this case, the comments are moved properly.
 **MacOS** has AppleScript, by which you can ask the Finder to perform the file copy/move. In this case, the comments are moved properly.
 
 
-## DEVELOPMENT STATUS
+## DEVELOPMENT STATUS{#status}
 
 
-Each app is a standalone file. That means there is a lot of redundancy between the tthree apps. And there _may_ be some inconsistency.
+Each app is a standalone file. That means there is a lot of redundancy between 
+the three apps. And there _may_ be some inconsistency.
 
 
 2022-10-04
 2022-10-04
 : All three apps are functioning and usable.  
 : All three apps are functioning and usable.  
-  The _config_file_ is fully implemented. 
-  Themes are not implemented. 
-  Comments are intended to be _utf-8_, but are _strings_ in some places. 
-  MacOS code is not written yet. 
-  The _help_ dialogs in <code>**dirnotes-tui**</code> are meagre.
+  The _config_file_ is fully implemented.  
+  Themes are not implemented.   
+  Comments are intended to be _utf-8_, but are _strings_ in some places.  
+  MacOS code is not written yet.   
+  The _help_ dialogs in <code>**dirnotes-tui**</code> are meagre.    
+  The _qt-gui_ app is working pretty well.  
+
+
+

File diff suppressed because it is too large
+ 626 - 483
dirnotes


+ 170 - 170
dirnotes-cli

@@ -9,197 +9,197 @@ answer_json = []
 verbose = 0
 verbose = 0
 db = None
 db = None
 xattr_comment = "user.xdg.comment"
 xattr_comment = "user.xdg.comment"
-xattr_author  = "user.xdg.comment.author"
-xattr_date    = "user.xdg.comment.date"
+xattr_author    = "user.xdg.comment.author"
+xattr_date      = "user.xdg.comment.date"
 
 
 #======= debugging/verbose ===========
 #======= debugging/verbose ===========
 def print_d(*a):
 def print_d(*a):
-  if verbose > 1:
-    print('>>',*a)
+    if verbose > 1:
+        print('>>',*a)
 def print_v(*a):
 def print_v(*a):
-  if verbose:
-    print('>',*a)
+    if verbose:
+        print('>',*a)
 
 
 #============= the functions that are called from the main.loop ===============
 #============= the functions that are called from the main.loop ===============
 
 
 def file_copy(f,target,target_is_dir,force):
 def file_copy(f,target,target_is_dir,force):
-  print_d(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_d(f"copy from {f} to {dest}")
+    print_d(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_d(f"copy from {f} to {dest}")
 
 
 def file_zap(f,all_flag):
 def file_zap(f,all_flag):
-  print_d(f"zapping the comment history of {f}")
-  if all_flag:
-    print_d("zapping the entire database")
+    print_d(f"zapping the comment history of {f}")
+    if all_flag:
+        print_d("zapping the entire database")
 
 
 def file_modify_comment(f, create, append, erase):
 def file_modify_comment(f, create, append, erase):
-  print_d(f"modify the comment on file {f} with extra={(create,append,erase)}")
-  if not os.path.exists(f):
-    print(f"the target file does not exist; please check the spelling of the file: {f}")
-    # sys.exit() here?
+    print_d(f"modify the comment on file {f} with extra={(create,append,erase)}")
+    if not os.path.exists(f):
+        print(f"the target file does not exist; please check the spelling of the file: {f}")
+        # sys.exit() here?
 
 
 def file_display(f, listall, history, json, minimal):
 def file_display(f, listall, history, json, minimal):
-  print_d(f"list file details {f}")
-  x_comment = None
-  try:
-    x_comment = xattr.getxattr(f,xattr_comment).decode()
-    x_author  = xattr.getxattr(f,xattr_author).decode()
-    x_date    = xattr.getxattr(f,xattr_date).decode()
-  except:
-    pass
-  full_f = os.path.realpath(f)
-
-  d_comment = getDbComment(full_f)
-  if d_comment:
-    d_comment, d_author, d_date = d_comment
-  print_d(f"for file {f}, database comment is <{d_comment}>, xattr comment is <{x_comment}>")
-
-  if os.path.isdir(f):
-    f = f+'/'
-  if x_comment or listall:
-    if x_comment and (d_comment != x_comment):
-      x_comment += '*'
-    if not json:
-      if minimal:
-        print(f"{x_comment}")
-      else:
-        print(f"{f}: {x_comment}")
-    else:
-      if verbose:
-        answer_json.append( {"file":f,"comment":x_comment,"author":x_author,"date":x_date } )
-      else:
-        answer_json.append( {"file":f,"comment":x_comment} )
+    print_d(f"list file details {f}")
+    x_comment = None
+    try:
+        x_comment = xattr.getxattr(f,xattr_comment).decode()
+        x_author    = xattr.getxattr(f,xattr_author).decode()
+        x_date      = xattr.getxattr(f,xattr_date).decode()
+    except:
+        pass
+    full_f = os.path.realpath(f)
+
+    d_comment = getDbComment(full_f)
+    if d_comment:
+        d_comment, d_author, d_date = d_comment
+    print_d(f"for file {f}, database comment is <{d_comment}>, xattr comment is <{x_comment}>")
+
+    if os.path.isdir(f):
+        f = f+'/'
+    if x_comment or listall:
+        if x_comment and (d_comment != x_comment):
+            x_comment += '*'
+        if not json:
+            if minimal:
+                print(f"{x_comment}")
+            else:
+                print(f"{f}: {x_comment}")
+        else:
+            if verbose:
+                answer_json.append( {"file":f,"comment":x_comment,"author":x_author,"date":x_date } )
+            else:
+                answer_json.append( {"file":f,"comment":x_comment} )
 
 
 def getDbComment(full_filename):
 def getDbComment(full_filename):
-  global db
-  print_d(f"db access for {full_filename}")
-  c = db.cursor()
-  c.execute("select comment,author,comment_date from dirnotes where name=? and comment<>'' order by comment_date desc",(full_filename,))
-  a = c.fetchone()
-  if a:
-    return a[0:3]
+    global db
+    print_d(f"db access for {full_filename}")
+    c = db.cursor()
+    c.execute("select comment,author,comment_date from dirnotes where name=? and comment<>'' order by comment_date desc",(full_filename,))
+    a = c.fetchone()
+    if a:
+        return a[0:3]
 
 
 def openDb():
 def openDb():
-  global db
-  dbName = "~/.dirnotes.db"
-  dbName = os.path.expanduser(dbName)
-  db = sqlite3.connect(dbName)
-  try:
-    c = db.cursor()
-    c.execute("select * from dirnotes")
-  except sqlite3.OperationalError:
-    c.execute("create table dirnotes (name TEXT, date DATETIME, size INTEGER, comment TEXT, comment_date DATETIME, author TEXT)")
-  return db 
+    global db
+    dbName = "~/.dirnotes.db"
+    dbName = os.path.expanduser(dbName)
+    db = sqlite3.connect(dbName)
+    try:
+        c = db.cursor()
+        c.execute("select * from dirnotes")
+    except sqlite3.OperationalError:
+        c.execute("create table dirnotes (name TEXT, date DATETIME, size INTEGER, comment TEXT, comment_date DATETIME, author TEXT)")
+    return db 
 
 
 def main(args):
 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='count',     help="verbose, almost debugging; do not use in scripts",default=0)
-  parser.add_argument('-j',"--json",    action="store_true",help="output in JSON format")
-  pars_m = parser.add_mutually_exclusive_group()
-  pars_m.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")
-  pars_m.add_argument('-c',"--create",  metavar="comment",  help="add a comment to a file")
-  pars_m.add_argument('-a',"--append",  metavar="comment",  help="append to a comment on a file, separator=';'")
-  pars_m.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")
-  pars_m.add_argument('-e',"--erase",   action="store_true",help="erase the comment on a file")
-  pars_m.add_argument('-z',"--zap",     action="store_true",help="clear the comment history on a file")
-  pars_m.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); list commands may omit this")
-  args = parser.parse_args()
-
-  # default is to display all files that have comments
-
-  # major modes are: display (<none> -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
-  
-  args.display = not (args.create or args.append or args.copy or args.erase or args.zap or args.zapall)
-  if args.cp_force and not args.copy:
-    print("the -y/--cp_force options can only be used with the -C/--copy command")
-    sys.exit(3)   
-  if args.json and not args.display:
-    print("the -j/--json option can only be used with the display modes")
-    sys.exit(4)
-  if args.minimal and not args.display:
-    print("the -n/--minimal option only applies to the display modes")
-    sys.exit(5)
-  if args.history and not args.display:
-    print("the -H/--history option only applies to the display modes")
-    sys.exit(5)
-  if args.xattr and (args.zap or args.zapall):
-    print("the -x/--xattr option doesn't apply to the -z/--zap and -Z/--zapall commands")
-    sys.exit(7)
-
-  global verbose
-  verbose = args.verbose
-  
-  #====== 1) build the file list =============
-
-  files = args.file_list
-  # for the list commands, auto-fill the file list with the current directory
-  if not files and args.display:
-    files = os.listdir(".")
-    files.sort()
-  # other command require explicity file lists, but 'dncli -c "new comment" *' will work
-  if not files:
-    print("please specify a file or files to use")
-    sys.exit(10)
-  print_d("got the files:", files)
-
-  #======= 2) build the function
-  if args.create or args.append or args.erase:
-    print_d(f"create/append/erase: {args.create} . {args.append} . {args.erase}")
-    func = file_modify_comment
-    loop_args = (args.create, args.append, args.erase)
-  elif args.zap or args.zapall:
-    print_d(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_d(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_d(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_d(f"copy from {files} to {target}")
-    if n_files > 2 and not target_is_directory:
-      print_d(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:
-    assert(args.display)
-    print_v(f"list files using {'database' if args.db else 'xattr'} priority")
-    print_d(f"display command with option: {args.listall} and {args.history} and {args.json} and {args.minimal}")
-    loop_args = (args.listall, args.history, args.json, args.minimal)
-    func = file_display
-
-  #====== 3) loop on the list, execute the function =============
-  global db
-  db = openDb()
-
-  for f in files:
-    func(f,*loop_args)
-
-  if answer_json:
-    print(json.dumps(answer_json))
+    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='count',           help="verbose, almost debugging; do not use in scripts",default=0)
+    parser.add_argument('-j',"--json",      action="store_true",help="output in JSON format")
+    pars_m = parser.add_mutually_exclusive_group()
+    pars_m.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")
+    pars_m.add_argument('-c',"--create",    metavar="comment",  help="add a comment to a file")
+    pars_m.add_argument('-a',"--append",    metavar="comment",  help="append to a comment on a file, separator=';'")
+    pars_m.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")
+    pars_m.add_argument('-e',"--erase",     action="store_true",help="erase the comment on a file")
+    pars_m.add_argument('-z',"--zap",           action="store_true",help="clear the comment history on a file")
+    pars_m.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); list commands may omit this")
+    args = parser.parse_args()
+
+    # default is to display all files that have comments
+
+    # major modes are: display (<none> -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
+    
+    args.display = not (args.create or args.append or args.copy or args.erase or args.zap or args.zapall)
+    if args.cp_force and not args.copy:
+        print("the -y/--cp_force options can only be used with the -C/--copy command")
+        sys.exit(3)     
+    if args.json and not args.display:
+        print("the -j/--json option can only be used with the display modes")
+        sys.exit(4)
+    if args.minimal and not args.display:
+        print("the -n/--minimal option only applies to the display modes")
+        sys.exit(5)
+    if args.history and not args.display:
+        print("the -H/--history option only applies to the display modes")
+        sys.exit(5)
+    if args.xattr and (args.zap or args.zapall):
+        print("the -x/--xattr option doesn't apply to the -z/--zap and -Z/--zapall commands")
+        sys.exit(7)
+
+    global verbose
+    verbose = args.verbose
+    
+    #====== 1) build the file list =============
+
+    files = args.file_list
+    # for the list commands, auto-fill the file list with the current directory
+    if not files and args.display:
+        files = os.listdir(".")
+        files.sort()
+    # other command require explicity file lists, but 'dncli -c "new comment" *' will work
+    if not files:
+        print("please specify a file or files to use")
+        sys.exit(10)
+    print_d("got the files:", files)
+
+    #======= 2) build the function
+    if args.create or args.append or args.erase:
+        print_d(f"create/append/erase: {args.create} . {args.append} . {args.erase}")
+        func = file_modify_comment
+        loop_args = (args.create, args.append, args.erase)
+    elif args.zap or args.zapall:
+        print_d(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_d(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_d(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_d(f"copy from {files} to {target}")
+        if n_files > 2 and not target_is_directory:
+            print_d(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:
+        assert(args.display)
+        print_v(f"list files using {'database' if args.db else 'xattr'} priority")
+        print_d(f"display command with option: {args.listall} and {args.history} and {args.json} and {args.minimal}")
+        loop_args = (args.listall, args.history, args.json, args.minimal)
+        func = file_display
+
+    #====== 3) loop on the list, execute the function =============
+    global db
+    db = openDb()
+
+    for f in files:
+        func(f,*loop_args)
+
+    if answer_json:
+        print(json.dumps(answer_json))
 
 
 if __name__ == "__main__":
 if __name__ == "__main__":
-  main(sys.argv)
+    main(sys.argv)
 
 
 
 

+ 1 - 0
dirnotes-cli.md

@@ -184,3 +184,4 @@ three attributes described in that file that are important:
 > * start_mode
 > * start_mode
 
 
 
 
+

Some files were not shown because too many files changed in this diff