summaryrefslogtreecommitdiff
path: root/jrnl
diff options
context:
space:
mode:
Diffstat (limited to 'jrnl')
-rwxr-xr-xjrnl262
1 files changed, 262 insertions, 0 deletions
diff --git a/jrnl b/jrnl
new file mode 100755
index 0000000..ff8e9a9
--- /dev/null
+++ b/jrnl
@@ -0,0 +1,262 @@
+#!/bin/bash
+# (c) 2019-2020 Vitaly Parnas <vp330@parnas.me>
+# See LICENSE for licensing information.
+
+#TAG_COLOR="00;36"
+TIMEFORMAT="%Y-%m-%d %H:%M"
+RECORD_START_PAT='[0-9]{4}-[0-9]{2}-[0-9]{2}\s[0-9]{2}:[0-9]{2}'
+
+# The 'tac' utility doesn't support perl-compatible regex
+TAC_RECORD_START_PAT='[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]\s[0-9][0-9]:[0-9][0-9]'
+
+declare -A JOURNALS=(
+ ['default']="$HOME/journals/journal.txt"
+)
+
+[ -f "$JRNL2_CFG" ] && CFG_FILE="$JRNL2_CFG" || CFG_FILE="$HOME/.jrnl2.rc"
+[ -f "$CFG_FILE" ] && . "$CFG_FILE"
+
+[ -z $DEFAULT_JRNL ] && DEFAULT_JRNL='default'
+JOURNAL="${JOURNALS[$DEFAULT_JRNL]}"
+[ -z "$JOURNAL" ] && echo "Journal '$DEFAULT_JRNL' not found" && exit 1
+[ ! -f "$JOURNAL" ] && echo "Cannot open $JOURNAL" && exit 1
+
+# Pass disjunctive arguments in regex (ie 'pat1|pat2' or '@(tag1|tag2)'
+# Pass conjunctive arguments as separate search -s operations, which are passed as separate arguments here
+ # awk '/foo/ && /bar/'
+ # grep -P '(?=.*?word1)(?=.*?word2)(?=.*?word3)^.*$'
+function jrnl_entry_search
+{
+ short_srch="$1"; shift
+ reverse="$1"; shift
+ max_entries="$1"; shift
+ tags_only="$1"; shift
+ case_sens="$1"; shift
+
+ $short_srch && instr='P' || instr='p'
+ $case_sens && mod='' || mod='I'
+ # case-insensitive match
+ search_cmd='sed -nr "/^'$RECORD_START_PAT'/{x;/./$instr;d; \${x;/./$instr;d}}; \${x;G;/./$instr}; {H}"'
+ while [ -n "$1" ]; do
+ pattern="$1"
+ search_cmd=$(sed -r 's/(\$instr)/{\/'"$pattern"'\/\$mod\1}/g' <<< "$search_cmd")
+ shift
+ done
+ if $tags_only; then
+ cmd_suffix+=" | list_tags"
+ else
+ $reverse &&
+ cmd_suffix+=" | tac -s '^$TAC_RECORD_START_PAT' --before --regex"
+ [ -n "$TAG_COLOR" ] &&
+ cmd_suffix+=" | GREP_COLORS='mt=$TAG_COLOR' grep --color -e '' -e '@\w\+'"
+ fi
+ if [ $max_entries -gt 0 ]; then
+ # Output entries in --short (1-line) mode to establish the start entry for the search space
+ prev_instr=$instr
+ instr='P'
+ start_line=$(eval "$search_cmd $JOURNAL | tail -$max_entries | head -1")
+ instr=$prev_instr
+ [ -z "$start_line" ] && return
+ # Use line number as start search space to avoid unescaped char issues. If completely duplicate headers exist, will choose the last as start space.
+ line_num=$(grep -nF "$start_line" $JOURNAL | awk -F':' '{print $1}' | tail -1)
+ sed -n "$line_num,\$p" "$JOURNAL" | eval "$search_cmd $cmd_suffix"
+ else
+ eval "$search_cmd $JOURNAL $cmd_suffix"
+ fi
+}
+
+function jrnl_entry_edit
+{
+ max_entries="$1"; shift
+ orig=$(jrnl_entry_search false false $max_entries false $case_sens "$@")
+ [ -z "$orig" ] && echo "Nothing found by that search" && return
+ tmpfile=$(mktemp --tmpdir jrnl-edit.XXXXXX)
+ trap 'rm "$tmpfile"' 0 1 15
+ echo "$orig" > "$tmpfile"
+ sed -i -r 's/^'"$RECORD_START_PAT"'.*/== MODIFY\/DELETE RECORD BELOW. DO NOT DELETE THIS LINE. \0 ==\n\n\0/' $tmpfile
+ $EDITOR $tmpfile
+ echo "$orig" | diff -q -BZ -I '==.*==' - $tmpfile \
+ >/dev/null && echo "No changes" && return
+ num_edited=0
+ num_deleted=0
+ num_intact=0
+ headers=$(sed -nr "s/^==.*($RECORD_START_PAT.*) ==$/\1/p" $tmpfile)
+ while read -r header; do
+ # Escape a bunch of characters conflicting with sed
+ header=$(sed -r 's/([?*+-/(){}|^$\\]|\[|\])/\\\1/g' <<< "$header")
+ old_record=$(sed -rn "/^$header/,/^$RECORD_START_PAT/{//!p;/^$header/p}" <<< "$orig")
+ new_record=$(sed -rn "/^==.*$header/,/^==/{//!{/^$RECORD_START_PAT/,\$p}}" "$tmpfile")
+ #echo -e "old:\n$old_record"
+ #echo -e "new:\n$new_record"
+ if [ "$old_record" = "$new_record" ]; then
+ (( num_intact++ ))
+ continue
+ fi
+ if [ -n "$new_record" ]; then
+ tmpjrnl=$(mktemp --tmpdir jrnl-temp.XXXXXX)
+ ( # Combine the section above entry + entry + section below
+ sed -rn '/^'"$header"'/,$!p' $JOURNAL
+ echo -e "$new_record""\n"
+ sed -r "1,/^$header/d" $JOURNAL |
+ sed -rn "/^$RECORD_START_PAT"'/,$p'
+ ) > "$tmpjrnl"
+ mv "$tmpjrnl" "$JOURNAL"
+ (( num_edited++ ))
+ else # delete old record
+ sed -i -nr "/^$header/,/$RECORD_START_PAT/{//!d;/^$header/d};p" $JOURNAL
+ (( num_deleted++ ))
+ fi
+ done <<< "$headers"
+ [[ $num_edited -gt 0 ]] && echo "Edited $num_edited entries"
+ [[ $num_deleted -gt 0 ]] && echo "Deleted $num_deleted entries"
+ [[ $num_intact -gt 0 ]] && echo "Intact $num_intact entries"
+}
+
+# Adds entry in preference of
+# 1. stdin
+# 2. argument
+# 3. Edited in $EDITOR
+function jrnl_entry_add
+{
+ # First, try stdin
+ entry=$(
+ while read -rt .01 stdin; do
+ echo "$stdin"
+ done
+ )
+ if [[ -z "$entry" ]]; then
+ if [[ -n $1 ]]; then
+ entry="${@:1}"
+ else
+ tmpfile=$(mktemp --tmpdir jrnl-new.XXXXXX)
+ trap 'rm "$tmpfile"' 0 1 15
+ $EDITOR $tmpfile
+ entry=$(cat $tmpfile)
+ #rm $tmpfile
+ fi
+ fi
+ if [[ -n "$entry" ]]; then
+ filter='1s/\.\s\+/.\n/' # Separate the first sentence from rest by a NL.
+ sed -rn "1{/^$RECORD_START_PAT/q0};q1" <<< "$entry" &&
+ prefix="" || prefix="$(date "+$TIMEFORMAT") "
+ echo -e "$prefix""$(sed "$filter" <<< "$entry")\n" >> "$JOURNAL"
+ echo "1 new entry written"
+ else
+ echo "Journal unmodified"
+ fi
+}
+
+# List all tags within provided journal file or otherwise stdin
+function list_tags {
+ [[ -n $1 ]] && input="$1" || input="-"
+ grep -E -o '(^|\s)@[_[:alnum:]\-]+' "$input" | sed 's/^\s*//' | sort -fib | uniq -ci | sort -nr
+}
+
+#
+## The stdout isn't immediately visible with each selected <ctrl>+<enter> tag as the entire output is piped to echo
+#alias jrnl_select_tags="jrnl --tags | awk '{print \$1}' | dmenu | xargs echo"
+
+function usage {
+ cat <<EOF
+${0##*/}:
+ -h|--help: help text,
+ -j|--journal <alternate journal (use -ls to list)>,
+ -ls|--list: list all available journals
+ -n|--max-entries <n>: limit query to the last n entries,
+ -s|--search <pattern>: search for records containing <pattern>. Can combine for a conjunctive search.
+ -S|--search-all <pattern>: search all journals for records containing <pattern>. Cannot combine with --edit.
+ -c|--case: case-sensitive search (insensitive by default)
+ -e|--edit: edit/delete the searched entries
+ -t|--tags: list of existing tags
+ -r|--short: view entry headings only
+ -R|--reverse: view entries in reverse
+EOF
+}
+
+function assert_args() {
+ cmd="$1"; shift
+ [[ $# -lt 1 || $1 =~ ^\- ]] && echo "$cmd needs an argument" && exit 128
+}
+
+short_srch=false
+max_entries=0
+tags_only=false
+case_sens=false
+edit=false
+search_all=false
+reverse=false
+search_args=()
+options_passed=false
+
+while [[ -n $1 ]]; do
+ case $1 in
+ -j|--journal)
+ shift
+ assert_args "journal" "$@"
+ if [ -n "${JOURNALS[$1]}" ]; then
+ JOURNAL="${JOURNALS[$1]}"
+ else
+ echo "Journal $1 undefined."
+ exit 1
+ fi
+ ;;
+ -t|--tags)
+ options_passed=true
+ tags_only=true;;
+ -c|--case)
+ options_passed=true
+ case_sens=true;;
+ -ls|--ls|--list)
+ for j in "${!JOURNALS[@]}"; do echo "$j"; done
+ exit 0;;
+ -r|--short)
+ options_passed=true
+ short_srch=true;;
+ -R|--reverse)
+ options_passed=true
+ reverse=true;;
+ -s|--search)
+ options_passed=true
+ shift
+ assert_args "search" "$@"
+ search_args+=("$1")
+ ;;
+ -S|--search-all)
+ options_passed=true
+ shift
+ assert_args "search-all" "$@"
+ search_args+=("$1")
+ search_all=true
+ ;;
+ -e|--edit)
+ options_passed=true
+ edit=true;;
+ -n|--max-entries)
+ options_passed=true
+ shift
+ assert_args "max_entries" "$@"
+ max_entries=$1
+ ;;
+ -h|--help|-*)
+ usage
+ exit 0;;
+ *) break;;
+ esac
+ shift
+done
+
+if ! $options_passed; then
+ jrnl_entry_add "$@"
+elif $search_all; then
+ for jrnl_label in "${!JOURNALS[@]}"; do
+ JOURNAL=${JOURNALS[$jrnl_label]}
+ echo "=== $jrnl_label ==="
+ jrnl_entry_search $short_srch $reverse $max_entries $tags_only $case_sens "${search_args[@]}"
+ done
+elif $edit; then
+ jrnl_entry_edit $max_entries "${search_args[@]}"
+elif $tags_only && [ -z "$search_args" ] && [ $max_entries == 0 ]; then
+ list_tags "$JOURNAL"
+else
+ jrnl_entry_search $short_srch $reverse $max_entries $tags_only $case_sens "${search_args[@]}"
+fi