#!/bin/bash PROFILE='default' function debug() { #echo $DEBUG if [ ${DEBUG:-0} -ne 0 ]; then echo "$(date): $@" fi } function help() { cat << EOF $0 - Captain's log Creates/opens files in which to log daily activity. Files are created with basic prompts. Create a file in "$CLDIR" called "schedule" (or use $0 schedule edit) with lines in the following format YYYY-MM-DD Task to review or = Recurring Task to review ; eg %A=Monday Task for monday OR using "$0 schedule " will achieve the same thing Create a file in "$CLDIR" called "template" with the base text to add to each day If the file is not executable, it is treated as a static template and is copied each day. The string {{DATE}} can be used to insert a date string. {{DATE }} can be used to replace the defualt format. i.e {{DATE %F}} fill produce the text $(date %F). If the file is executable, it will be run in the context of the user, and the output will be used instead. Create a file in "$CLDIR" named "backlog", and it will be opened along with the other daily files, split under "tomorrow" Directories created under $BASEDIR are treated as profiles (if they exist). The default profile is assumed to be 'default'. Specify -p as the first argument to $0 to operate on a different one Run with a date-compatible string to generate/open only that file i.e, $0 today; $0 tuesday; etc Run with "asset" to spin up a new empty file with an asset tag. This is currently assumed to be plain text that complements the log, but makes little sense to included. At some point this will be expanded to handle other files Run with "asset " to open said asset. Run with "review" ($0 review), and you will get a rundown of complete and incomplete tasks for the week. Optionally, supply with date-compatible strings as "$0 review [end] to use a range, end defaults to yesterday A task is considered complete if it matches the regex /^ \*/ and incomplete matching /* \-/ Also considering + to mean "a task that was not planned, but popped up during the course of the day". For the purposes of review it counts as incomplete, unless prefixed with \* I've also implemented /^ ~/, implying a continuing task (as opposed to - which I'm sort of considering "not only incomplete, but barely progressed/attempted"). For the purposes of review, ~ counts as Completed. I'm hoping to capture which tasks are is under-estimated. Events are also printed, assuming ! below Notes are generally ignored, but can be started with "^ #", "^ ;", or "^ !" for future functionality This will evolve with use, but generally (and in order of importance) ! signifies and event worthy of logging ; is a general note possibly worth reviewing # is for a verbose note, probably not of general interest but was worth noting If $CLDIR is a git repo (git rev-parse returns 0), it will be updated and auto committed unless --no-git is passed TAGS can be created by prefixing the name of the tag with '//' This stores a list of generated links to the tag in an asset. They can then be reviewed with "$0 tag review " This allows for a quick view of the tag's history. Consider it like a project or ongoing task Reminders for tasks not completed the day before are transferred to the following day EOF } function review() { DATE_START=${1:-"-7 days"} DATE_END=${2:-"yesterday"} DATE_RANGE=$(( ($(date -d "$DATE_END" "+%s") - $( date -d "$DATE_START" "+%s")) /86400 )) if [ $DATE_RANGE -lt 0 ]; then echo "DATE_END is earlier than DATE_START" exit 1 fi # Make an array of possible dates to get stuff from declare -a DATES # This is stupid. {n..n} notation doesn't work with variables (no substitution) and seq doesn't do reverse ranges # so we use seq to get the range, then reverse it with tac for i in $(seq 0 $DATE_RANGE|tac ); do DATES[${#DATES[@]}]="$(date -d"$DATE_END -$i days" "+%F")" done for j in '\*|~' '-|\+' '!' ; do case $j in \\*\|\~) echo "Completed Tasks:";; \-\|\\+) echo "Incomplete Tasks:";; \!) echo "Events:";; esac for i in ${DATES[@]} ; do if [ -f "$CLDIR/$i" ]; then T="$(grep -P "^\s($j\s)" "$CLDIR/$i")" if [ "$T" != "" ]; then echo -n " ";date "+%A %d %B %Y" -d "$i" echo "$T"|sed "s/^/\t/" fi fi done echo done } function schedule() { if [ ${1:--1} -eq -1 ] || [ "$1" == "edit" ]; then if [ -f "$CLDIR/schedule"* ];then vi -p "$CLDIR/schedule"* else vi "$CLDIR/scheduled" fi exit fi if ! [[ $1 =~ = ]] && [ "$(date -d "$1" "+%s")" -lt $(date -d "today" "+%s") ]; then echo -n "$1 Looks like it's in the past, do you still wan to add it? [y/N]: " read answer if ! [[ "$answer" =~ ^[yY] ]]; then exit fi fi echo "$@" >> "$CLDIR/scheduled" } function prev_working_day() { if [ "$(date ${1+-d "$1"} +%a)" == 'Mon' ]; then modY=3;fi date +%F -d "$1-${modY:-1} day" } function today() { date ${1+-d "$1"} +%F } function next_working_day() { if [ "$(date ${1+-d "$1"} +%a)" == 'Fri' ]; then modT=3;fi date +%F -d "${1}+${modT:-1} day" } function captains_log() { # Take into account only the work week. # Might make sense in the future to check if there are any notes for the intervening days Sat/Sun # and display them if so YESTERDAY=$(prev_working_day) TODAY=$(today) TOMORROW=$(next_working_day) if [ ${GIT:-1} ] && (git -C $CLDIR rev-parse 2>/dev/null) ; then GIT=1 else GIT=0 fi if ! [ -d "$CLDIR" ]; then mkdir "$CLDIR" fi if [ $GIT -ne 0 ]; then git -C "$CLDIR" pull -q & fi # Create files if they don't exist if [ ${1-x} == 'x' ]; then EDIT_FILES=$(echo $CLDIR/{$YESTERDAY,$TODAY,$TOMORROW}) for i in YESTERDAY TODAY TOMORROW; do generate_file ${!i} if [ $i == 'TODAY' ]; then transfer_items $CLDIR/$YESTERDAY $CLDIR/$TODAY fi done if [ -f "$CLDIR/backlog" ]; then vi $CLDIR/{$YESTERDAY,$TODAY,$TOMORROW,backlog} -s <( echo -e ":vsplit\n:wincmd w\n:next\n:vsplit\n:wincmd w\n:next\n:split\n:wincmd w\n:next") else vi -O $CLDIR/{$YESTERDAY,$TODAY,$TOMORROW} fi else D=$(date -d "$1" "+%F") EDIT_FILES=$CLDIR/$D generate_file $D transfer_items $CLDIR/$(prev_working_day) $CLDIR/$D vi $CLDIR/$D fi asset generate_links for i in $(grep -oP '\s//[^\s]+' $EDIT_FILES|sort -u); do tag ${i#//} 2>/dev/null done tag generate_links if [ $GIT -ne 0 ]; then echo Updating git... if [ $( git -C "$CLDIR" status -s |wc -l) -ne 0 ]; then git -C "$CLDIR" add -A git -C "$CLDIR" commit -qm "Auto-Commit by $(basename $0)" git -C "$CLDIR" push -q fi fi } function generate_file() { # Create files if they don't exist F=$(date -d "${1:-$(date +%F)}" "+%F") if ! [ -f "$CLDIR/${F}" ]; then # If there's a template and it's executable, execute it and output the result to the file, # If it's not executable, replate {{DATE }} with said date/formate # Otherwise do the boilerplate debug "File $F doesn't exist, creating it" if [ -x "$CLDIR/template" ]; then debug "From executable template" "$CLDIR/template" > $CLDIR/${F} elif [ -f "$CLDIR/template" ]; then DATE_ARGS="$(grep '{{DATE[^}]*}}' "$CLDIR/template"|tr -d {}|cut -d' ' -f2-)" if [ "$DATE_ARGS" == 'DATE' ]; then DATE_ARGS='%A %d %B %Y'; fi debug "From static template with DATE_ARGS=$DATE_ARGS" sed "s/{{DATE[^}]*}}/$(date "+$DATE_ARGS" -d ${F})/g" $CLDIR/template > $CLDIR/${F} else echo -e "$(date "+%A %d %B %Y" -d ${!F})\nWhat do you want to accomplish today?\n\nWhat are your notes for today?\n\nWhat do you need to follow up tomorrow?\n" > "$CLDIR/${!F}" fi fi # Read from the "scheduled" file, and put lines into the appropriate file if extant if [ -f "$CLDIR/scheduled" ] || [ -f "$CLDIR/schedule" ]; then while read line; do SCHEDULED=0 SCHED=$(echo $line|cut -d' ' -f1) TASK=$(echo $line|cut -d' ' -f2-) # Check if the schedule is DATE_FORMAT=MATCH_EXPRESSION otherwise assume %F if ( [[ "$SCHED" =~ = ]] && [[ "$( date "+$(echo "$SCHED"|cut -d'=' -f1)" -d "${F}" )" =~ $(echo $SCHED |cut -d'=' -f2-) ]] ) || ( ! [[ "$SCHED" =~ = ]] && [ "$(date +%F -d $SCHED)" == "${F}" ] ); then if ! grep -qE "^Scheduled to Review today:" "$CLDIR/${F}"; then echo "Scheduled to Review today:" >> "$CLDIR/${F}" fi if ! grep -qE "^ . $TASK" "$CLDIR/${F}";then echo " - $TASK" >> "$CLDIR/${F}" fi if ! [[ "$SCHED" =~ = ]]; then SCHEDULED=1 fi fi if [ $SCHEDULED -eq 0 ]; then echo "$line" >> "$CLDIR/scheduled.tmp" fi done < <(cat "$CLDIR/scheduled" "$CLDIR/schedule" 2>/dev/null) fi # In theory, either everyhing is scheduled OR in the tmp file, so we delete it and do a swapsie rm "$CLDIR/scheduled" "$CLDIR/schedule" 2> /dev/null if [ -f "$CLDIR/scheduled.tmp" ]; then mv "$CLDIR/scheduled.tmp" "$CLDIR/scheduled" fi } function asset_generate_links() { # Either do files modified in the last 7 days, or all files if [ "$1" == "all" ]; then MTIME='' else MTIME='-mtime -7' fi # Exclude things we don't care about (swap files, git, the db itself) FILES=$(find ~/.captains_log/default/ -type f $MTIME \! \( -iname *.sw* -o -wholename "*/.git*" -wholename "*/assets/db" \)) # Look for references to assets MATCHES="$(grep -oE '\[?ASSET:)?[a-fA-F0-9]+\]?' $FILES)" while read line; do # Nicely format things FILE="$(echo $line | cut -d':' -f1)" FILE="${FILE#$CLDIR/}" REF="$(echo $line|cut -d ':' -f2-|grep -oE '[a-fA-F0-9]{8}')" # Assets contain a self reference so that it can be easily copied while editing. Ignore these refs if ! [[ "$REF" =~ $(basename $FILE 2>/dev/null) ]]; then DB_STRING="$REF:link $FILE" #Add it to the DB if it's not already there if ! grep -q "$DB_STRING" $ADB 2>/dev/null; then echo $DB_STRING >> $ADB fi fi done < <(echo "$MATCHES") } function asset() { export ASSET_DIR="$CLDIR/assets/" export ADB="$ASSET_DIR/db" while [ $1 ]; do case $1 in generate_links) shift; asset_generate_links $@; return;; tag) TYPE=tag; shift;; *) break;; esac done if [ ${1:-0} != 0 ] && [[ $1 =~ (^\[?ASSET:)?[a-fA-F0-9]{8}\]? ]]; then ID="$(echo "$1"|grep -oE '[a-fA-F0-9]{8}')" vi "$ASSET_DIR/$ID" else if ! [ -d "$ASSET_DIR" ]; then mkdir "$ASSET_DIR" touch $ADB fi # Generate a random ID that doesn't already exist while grep -qi "$ID" "${ADB}"; do ID="$(openssl rand -hex 4)" # Alternatively #hexdump -n 4 -e '/4 "%08X" 1 "\n"' /dev/urandom done echo "$ID:type ${TYPE-text}" >> "$ADB" for i in $@; do K="$(echo $@|cut -d= -f1)" V="$(echo $@|cut -d= -f2-)" echo "$ID:$K $V" >> "$ADB" done echo "[ASSET:$ID]" > "$ASSET_DIR/$ID" if [ "$TYPE" != 'tag' ]; then vi "$ASSET_DIR/$ID" fi fi } function tag() { while [ $1 ]; do case $1 in generate_links) shift; tag_generate_links $@; return;; review) shift; tag_review $@; return;; *) break;; esac done if [ "$(tag_get_id $1)" == "" ]; then asset tag "name=$1" echo "Created tag $1" else echo "Tag $1 already exists: $(tag_get_id $1)" >&2; fi } function tag_generate_links() { export ASSET_DIR="$CLDIR/assets/" export ADB="$ASSET_DIR/db" # Either do files modified in the last 7 days, or all files if [ "$1" == "all" ]; then MTIME='' else MTIME='-mtime -7' fi # Exclude things we don't care about (swap files, git, the db itself) FILES=$(find ~/.captains_log/default/ -type f $MTIME \! \( -iname *.sw* -o -wholename "*/.git*" -wholename "*/assets/db" \)) # Get the asset ID for each tag, store it in an array declare -A REFS eval REFS=($(for i in $(grep -E '[a-fA-F0-9]{8}:type tag' $ADB|cut -d: -f1); do grep $i:name $ADB|sed 's/^\([a-fA-F0-9]\{8\}\):name \(.*\)/[\2]=\1/'; done)) # Look for references to assets MATCHES="$(grep -oP '\s//[^\s]+' $FILES)" while read line; do # Nicely format things FILE="$(echo $line | cut -d':' -f1)" FILE="${FILE#$CLDIR/}" TAG="$(echo $line|cut -d ':' -f2-|grep -oP '//[^\s]+'|tr -d '/')" if [ "${REFS[$TAG]}" == '' ]; then echo "$TAG from $FILE doesn't exist. Skipping" continue fi # Assets contain a self reference so that it can be easily copied while editing. Ignore these refs if ! [[ "$REF" =~ $(basename $FILE 2>/dev/null) ]]; then DB_STRING="${REFS[${TAG}]}:link $FILE" #Add it to the DB if it's not already there if ! grep -q "$DB_STRING" $ASSET_DIR/${REFS[${TAG}]} 2>/dev/null; then echo $DB_STRING >> $ASSET_DIR/${REFS[${TAG}]} fi fi done < <(echo "$MATCHES") } function transfer_items() { TRANSFER='' while read line1; do while read line2; do MATCH=0 ; VAL=$(echo "$(fstrcmp -- "$line1" "$line2") * 100"|bc -l|cut -d . -f1 ); if [ $VAL -gt 60 ]; then MATCH=1 break fi; done < <(grep '^\s*[~-]' $2); if [ $MATCH -eq 0 ]; then TRANSFER="${TRANSFER}\n$line1" fi done < <(grep '^\s*[~-]' $1) if [ "$TRANSFER" != '' ]; then echo -e "\nThese incomplete/ongoing items have been transferred from $(basename "$1"):\n $TRANSFER" >> $2 fi } function tag_get_mentions() { debug "getting tag mentions for $@" F=$1; shift unset BLOCKS declare -a TAGS # Get a list of things that look like tasks TASKS=($(grep -nE '^ ' $F |cut -d: -f1)) debug "TASKS ${TASKS[@]}" # Set last element of the array to be EOF, so that if it matches the last block the below works TASKS[${#TASKS[@]}]=$(wc -l $F|cut -d' ' -f1) # Compare them to the line numbers given as arguments for line in $@; do debug LINE: $line for i in $(seq 0 $((${#TASKS[@]} -1 )) ); do if [ $(( ${TASKS[$i]} >= $line )) == 1 ]; then if [ $(( ${TASKS[$i]} > $line )) == 1 ]; then debug "LINE_MATCH_GT: $line > ${TASKS[$i]}" BLOCKS="$BLOCKS ${TASKS[$(( $i - 1 ))]}"; else debug "LINE_MATCH_EQ: $line = ${TASKS[$i]}" BLOCKS="$BLOCKS ${TASKS[$i]}"; fi break fi done done BLOCKS="$(echo "$BLOCKS"|tr ' ' "\n"|sort -n|uniq)" debug "BLOCKS $BLOCKS" for BLOCK_START in $BLOCKS; do # Grab from the preceding start of yaml element, to the first line that doesn't start with two pieces of whitespace (i.e the yaml block) BLOCK="$(sed -rn -e $BLOCK_START',/^ ?\S/p' $F|sed '$d')" debug "BLOCK STARTING AT $BLOCK_START\n $BLOCK" echo -e "$BLOCK"|fmt -w 100 done } function tag_get_id() { export ASSET_DIR="$CLDIR/assets/" export ADB="$ASSET_DIR/db" echo $(for i in $(grep ":name $1" $ADB|cut -d: -f1); do grep $i $ADB|grep ':type tag'|cut -d: -f1;done) } function tag_review() { export ASSET_DIR="$CLDIR/assets/" export ADB="$ASSET_DIR/db" cd $CLDIR ID=$(tag_get_id $1) for i in $(grep ':link' $ASSET_DIR/$ID|cut -d' ' -f2-); do echo $i tag_get_mentions $i $(grep -n //$1 $i|cut -d: -f1) echo done } BASEDIR="$HOME/.captains_log" # Check if there are any profiles, otherwise just use the basedir if [ "$(find $BASEDIR -type d|wc -l)" -gt 1 ]; then CLDIR="${BASEDIR}/${PROFILE:-default}" else CLDIR="$BASEDIR" fi while [ $1 ]; do case $1 in -p|--profile) CLDIR="$BASEDIR/$2"; shift 2;; --no-git) GIT=0;shift;; --debug) DEBUG=1; shift;; review) shift; review "$@"; exit;; schedule) shift; schedule "$@";exit;; asset) shift; asset "$@"; exit;; tag) shift; tag "$@"; exit;; *ASSET*) asset "$@"; exit;; help|--help|-h) help; exit;; transfer) transfer_items $CLDIR/$(today) $CLDIR/$(next_working_day); exit;; dir) echo $CLDIR; exit;; *) break;; esac done captains_log $@