Zsh goodies for j.d

In zsh we can levy the power of named folders to ease the task of dealing with Johnny.Decimal folders.

There are two approaches to this:

  • First, figure out at each call if there is a jd folder corresponding to the number
  • Second, create a hash table that is renewed periodically (or better, using incron), since jd folders don’t change on a daily basis.

In the following posts, I will detail the two approaches, as I’ve been using the first for some months now, but am passing on to the second, as the first causes unnecessary lag imho.

THE FIRST APPROACH IS HERE FOR HISTORIC AND PAEDAGOGICAL REASONS ONLY

In the end, you get something like that (yes, I know, I’ve got a lot of stuff to do):

First approach

The first approach uses the function zsh_directory_name which can be defined, say, in .zshrc, so that ~[jXX.YY] or ~[jXX] are computed and understood as referring to Johnny.Decimal folders. As computations are kinda slow, this method is only recommended if you create j.d folders very often. I am myself transitioning to the second approach. Here is the commented code below for this approach. Do not hesitate to ask for further as this bit of code is slightly obscure if you haven’t read the zsh docs.

# named directories for Johnny.Decimal
JD_ROOT=/home/user/docs

function j_check_depth {
  # This function checks if the depth for a j.d folder is correct
  # The error code is used for use in `if` statements
  # $1 is id
  # $2 is path
  local depth="$(realpath -Ls --relative-to "${JD_ROOT}" "${2}" | awk -F/ '{print NF}')"
  if [[ ${1%%.*} = $1 ]]; then
    (( ($1 % 10) == 0 )) && [[ $depth -eq 1 ]] || [[ $depth -eq 2 ]]
  else
    [[ $depth -eq 3 ]]
  fi
}

function zsh_directory_name {
  # check whether we are in a named directory situation
  if [[ $1 = n ]]; then
    # check if we are trying to resolve a j.d folder
    if [[ $2 == j* ]]; then
      local dir
      local -a list_dir
      # define an array of all possible dirs within JD_ROOT
      list_dir=(${JD_ROOT}/**/${2:1}*)

      # here we build a reply, returning 0 is it is successful
      # see zsh docs for the requirements
      typeset -ga reply
      for dir in $list_dir; do
        if [[ -d "${dir}" ]] && j_check_depth "${2:1}" "${dir}"; then
          reply=("${dir}")
          return 0
        elif [[ -f "${dir}" ]] && j_check_depth "${2:1}" "${dir}"; then
          reply=("$(dirname "${dir}")")
          return 0
        fi
      done
    fi

  # Here we define the name of the directories as they may appear in the prompt
  # Given a folder XX(.YY)?-name, this outputs ~[jXX\1|$name].
  elif [[ $1 = d ]]; then
    if [[ $2 =~ $JD_ROOT'/[0-9]0.+' ]]; then
      local finished=false dir="$2" number=-1 name=null
      while (( ${#dir} > 1 )) && ! $finished; do
        bname="$(basename "${dir}")"
        if [[ "$bname" =~ '^[0-9]{2}(.[0-9]{2})?-.+$' ]]; then
          number=${bname%%-*}
          name=${bname#*-}
          finished=true
        else
          dir="$(dirname "${dir}")"
        fi
      done
      typeset -ga reply
      reply=("j$number|$name" ${#dir})
      return 0
    fi
  fi
  return 1
}

Second approach

The second approach is less exact. It uses a function to build a dictionary of named directories

function isJD { [[ "$1" =~ '^[0-9]{2}(\.[0-9]{2,})?-.+$' ]] }
function get_jdnum { echo "$1" | cut -d- -f1 }

function makeJDvars {
  # echoes commands to make the named directories
  for jdpath in $(find docs -maxdepth 3 -type d -iregex 'docs/[0-9][0-9].+');
  do
    jd_folder="$(basename $jdpath)"
    if isJD $jd_folder; then
      jdnum="$(get_jdnum $jd_folder)"
      echo "hash -d j$jdnum='/home/ax/$jdpath'"
    else
       echo $jdpath is not j.d >&2
    fi
  done
}

source $ZDOTDIR/jdfolders

The function makeJDvars should be either called periodically or at each opening of zsh, depending on what you prefer. One of the drawbacks of using this approach is that it hides the name part of the directory. This is easily fixed by using a modified version of the script in the first version

function zsh_directory_name {
 if [[ $1 = d ]]; then
   folder="$(basename $2)"
   if isJD $folder; then
     jdnum="$(get_jdnum $folder)"
     name="$(echo $folder | cut -d- -f2)"
     typeset -ga reply
     reply=("j$jdnum|$name" ${#2})
     return 0
   fi
 fi
 return 1
}

any example or screenshot for how the 2nd approach works or what it looks like? This seems like something I would like, but am unsure…

The screenshot in the first post is one the 2nd method. Basically all ~jXX(.YY)? is a directory: you can cp it, mv it, cd in it, etc, etc.

The second part of the code is just for a e s t h e t i c s.

I’ve found this useful, and thought others might.

As part of moving things into a JD system, I’ve been putting files into my Dropbox folder, but there are some files that I don’t want to sync (for example, large files like downloaded movies - I have limited space, and if I lose them I can download them again in the future).

The Dropbox docs explain how to ignore individual files.

On a mac you can use
xattr -w com.dropbox.ignored 1 citizen_kane.mp4
to ignore a file, or
xattr -wr com.dropbox.ignored 1 movies/
to ignore all the files in a directory and its subdirectories.

You can use xattr -l to list the extended attributes on a file (including “com.dropbox.ignored” if it is there), which could be useful if you have some automated way to build your index and want to check if it something is synced with Dropbox or not.

1 Like

This is a really useful thread all round, but I particularly wanted to thank @bbfile for this Dropbox tip. I wish I had know about this ages ago, as it would have save me a lot of fiddling around with symlinks!

I wanted to exclude my Financial and Medical directories from Dropbox for privacy and security reasons (and because I don’t need to consult them on other devices), and this works perfectly: I can sync the rest of my JD directory but just store these two areas locally.

1 Like

Just curious, how do you back these up? Locally somehow I presume, do you also trust e.g. Backblaze with its option to encrypt before it leaves your device?

Yes, I have a Time Machine backup and also a local clone (using Carbon Copy Cloner) to a USB drive. Both backup disks use FileVault encryption, and both backup everything on my drive. Then I also (belt, braces and emergency piece of string!) back up to Backblaze, using a private encryption key, as an off-site backup.

I do find it useful to have the other files in Dropbox, because occasionally I need to access some of them on other devices. I keep all my manuals on there for example, and also PDF sewing patterns. If I find myself in a fabric shop, and see some fabric I think might be good for a particular pattern I own, I can open the PDF on my phone and check what length I would need to buy to make the garment, and what other haberdashery items I would need (interfacing, zips, buttons etc.). It takes the guesswork out of impulse fabric purchases! Not that I have been able to get to a bricks and mortar fabric shop in over a year :sob: .

I use fasd or any other variant of z jumping. It lets you jump to any JD folder instantly. If you wanna get really fancy you can manually weight the “frecency” of your folders but I’ve never had any collisions.

A small update concerning this. Using the following function with names defined as above:

function j {
  pushd ~j$1
}

I’ve defined the following completion function which fetches longer descriptions from the index file using the module ~xaltsc/johnny.scm - sourcehut hg

_j () {
  local ret=1
  local -a dir_map
  dir_map=("${(@f)$(~j91.03/johnny.scm --complete)}")
  _describe 'johnny.decimal' dir_map && ret=0
  return $ret
}

(where the module is stored in my 91.03 folder).