b298cffedbdd52cfaad9120522ac6863400d4cc3
[bash-notes.git] / notes.sh
1 #! /bin/bash
2
3 # bash-notes © 2023 by danix is licensed under CC BY-NC 4.0.
4 # To view a copy of this license, visit http://creativecommons.org/licenses/by-nc/4.0/
5
6 # to debug the script run it like:
7 # DEBUG=true notes.sh ...
8 # and check /tmp/debug_bash-notes.log
9 if [[ $DEBUG == true ]]; then
10 exec 5> /tmp/debug_bash-notes.log
11 BASH_XTRACEFD="5"
12 PS4='$LINENO: '
13 set -x
14 fi
15
16 PID=$$
17 BASENAME=$( basename "$0" )
18 NOW=$(date +%s)
19
20 VERSION="0.3"
21 DBVERSION=${VERSION}_${NOW}
22
23 set_defaults() {
24 # Binaries to use
25 JQ=${JQ:-/usr/bin/jq}
26 EDITOR=${EDITOR:-/usr/bin/vim}
27 TERMINAL=${TERMINAL:-/usr/bin/alacritty}
28 # add options for your terminal. Remember to add the last option to execute
29 # your editor program, otherwise the script will fail.
30 # see example in the addnote function
31 TERM_OPTS="--class notes --title notes -e "
32 # Setting PAGER here overrides whatever is set in your default shell
33 # comment this option to use your default pager if set in your shell.
34 PAGER=${PAGER:-/usr/bin/more}
35
36 # set this to true to have output in plain text
37 # or use the -p option on the command line before every other option
38 PLAIN=false
39 # base directory for program files
40 BASEDIR=${BASEDIR:-~/.local/share/bash-notes}
41 # notes database in json format
42 DB=${BASEDIR}/db.json
43 # directory containing the actual notes
44 NOTESDIR=${BASEDIR}/notes
45
46 } # end set_defaults, do not change this line.
47
48 set_defaults
49
50 # Do not edit below this point
51 RCFILE=${RCFILE:-~/.config/bash-notes.rc}
52 TMPDB=/tmp/db.json
53
54 if [ ! -x "$JQ" ]; then
55 echo "jq not found in your PATH"
56 echo "install jq to continue"
57 exit 1
58 fi
59
60 # IMPORT USER DEFINED OPTIONS IF ANY
61 if [[ -f $RCFILE ]]; then
62 # shellcheck disable=SC1090
63 source "$RCFILE"
64 fi
65
66 # We prevent the program from running more than one instance:
67 PIDFILE=/var/tmp/$(basename "$0" .sh).pid
68
69 # Make sure the PID file is removed when we kill the process
70 trap 'rm -f $PIDFILE; exit 1' TERM INT
71
72 if [[ -r $PIDFILE ]]; then
73 # PIDFILE exists, so I guess there's already an instance running
74 # let's kill it and run again
75 # shellcheck disable=SC2046,SC2086
76 kill -s 15 $(cat $PIDFILE) > /dev/null 2>&1
77 # should already be deleted by trap, but just to be sure
78 rm "$PIDFILE"
79 fi
80
81 # create PIDFILE
82 echo $PID > "$PIDFILE"
83
84 # Export config to file
85 function export_config() {
86 if [ -r ${RCFILE} ]; then
87 echo "Backing up current '${RCFILE}'...."
88 mv -f ${RCFILE} ${RCFILE}.$(date +%Y%m%d_%H%M)
89 fi
90 echo "Writing '${RCFILE}'...."
91 sed -n '/^set_defaults() {/,/^} # end set_defaults, do not change this line./p' $0 \
92 | grep -v set_defaults \
93 | sed -e 's/^\([^=]*\)=\${\1:-\([^}]*\)}/\1=\2/' \
94 > ${RCFILE}
95 if [ -r ${RCFILE} ]; then
96 echo "Taking no further action."
97 exit 0
98 else
99 echo "Could not write '${RCFILE}'...!"
100 exit 1
101 fi
102 }
103
104 # we should expand on this function to add a sample note and explain a little bit
105 # how the program works.
106 function firstrun() {
107 [ -f $RCFILE ] && RC=$RCFILE || RC="none"
108
109 clear
110 echo "${BASENAME} configuration:
111
112 base directory: ${BASEDIR}/
113 notes archive: ${NOTESDIR}/
114 notes database: ${DB}
115 rc file: $RC
116 text editor: ${EDITOR}
117 terminal: ${TERMINAL}
118 jq executable: ${JQ}
119 "
120
121 read -r -p "Do you wish to continue? (y/N) " ANSWER
122 case $ANSWER in
123 y|Y )
124 mkdir -p $NOTESDIR
125 cat << __EOL__ > ${DB}
126 {
127 "params": {
128 "version": "${VERSION}",
129 "dbversion": "${DBVERSION}"
130 },
131 "notes": []
132 }
133 __EOL__
134 echo; echo "All done, you can now write your first note."
135 ;;
136 * )
137 echo "No changes made. Exiting"
138 exit
139 ;;
140 esac
141 }
142
143 # check for notes dir existance and create it in case it doesn't exists
144 if [[ ! -d $NOTESDIR ]]; then
145 # we don't have a directory. FIRST RUN?
146 firstrun
147 fi
148 # check if input is a number, returns false or the number itself
149 function check_noteID() {
150 IN=$1
151 case $IN in
152 ''|*[!0-9]*)
153 return 1
154 ;;
155 *)
156 echo "$IN"
157 ;;
158 esac
159 }
160
161 function helptext() {
162 echo "Usage:"
163 echo " $0 [PARAMS] ..."
164 echo ""
165 cat << __NOWCONF__
166 ${BASENAME} configuration is:
167
168 base directory: ${BASEDIR}/
169 notes archive: ${NOTESDIR}/
170 notes database: ${DB}
171 rc file: $RCFILE
172 debug file: /tmp/debug_bash-note.log
173
174 text editor: ${EDITOR}
175 terminal: ${TERMINAL}
176 jq executable: ${JQ}
177 __NOWCONF__
178
179 echo ""
180 echo "${BASENAME} parameters are:"
181 echo " -h | --help : This help text"
182 echo " -p | --plain : Output is in plain text"
183 echo " (without this option the output is formatted)"
184 echo " (this option must precede all others)"
185 echo " -l | --list : List existing notes"
186 echo " -a | --add [\"<title>\"] : Add new note"
187 echo " -e | --edit [<note>] : Edit note"
188 echo " -d | --delete [<note> | all] : Delete single note or all notes at once"
189 echo " -s | --show [<note>] : Display note using your favourite PAGER"
190 echo " -v | --version : Print version"
191 echo " --userconf : Export User config file"
192 echo ""
193 }
194 function addnote() {
195 # remove eventually existing temp DB file
196 if [[ -f $TMPDB ]]; then
197 rm $TMPDB
198 fi
199
200 NOTETITLE="$1"
201 echo "adding new note - \"$NOTETITLE\""
202 # shellcheck disable=SC2086
203 LASTID=$($JQ '.notes[-1].id // 0 | tonumber' $DB)
204 # [ "" == $LASTID ] && LASTID=0
205 NOTEID=$(( LASTID + 1 ))
206 # shellcheck disable=SC2086
207 touch ${NOTESDIR}/${NOW}
208 # shellcheck disable=SC2016
209 $JQ --arg i "$NOTEID" --arg t "$NOTETITLE" --arg f "$NOW" '.notes += [{"id": $i, "title": $t, "file": $f}]' "$DB" > $TMPDB
210 # shellcheck disable=SC2086
211 mv $TMPDB $DB
212 # example for alacritty:
213 # alacritty --class notes --title notes -e /usr/bin/vim ...
214 # shellcheck disable=SC2086,SC2091
215 $(${TERMINAL} ${TERM_OPTS} ${EDITOR} ${NOTESDIR}/${NOW})
216 }
217 function editnote() {
218 NOTE=$1
219 # shellcheck disable=SC2155
220 local OK=$(check_noteID "$NOTE")
221 if [ ! "$OK" ]; then
222 echo "invalid note \"$NOTE\""
223 echo "Use the note ID that you can fetch after listing your notes"
224 exit 1
225 fi
226
227 # shellcheck disable=SC2016,SC2086
228 TITLE=$($JQ --arg i $OK '.notes[] | select(.id == $i) | .title' $DB)
229 # shellcheck disable=SC2016,SC2086
230 FILE=$($JQ -r --arg i $OK '.notes[] | select(.id == $i) | .file' $DB)
231 if [ "$TITLE" ]; then
232 echo "editing note $TITLE"
233 # shellcheck disable=SC2086,SC2091
234 $(${TERMINAL} ${TERM_OPTS} ${EDITOR} ${NOTESDIR}/${FILE})
235 else
236 echo "note not found"
237 exit 1
238 fi
239 exit
240 }
241 function listnotes() {
242 # [ $PLAIN == true ] && echo "output is plain text" || echo "output is colored"
243 if [[ $(ls -A "$NOTESDIR") ]]; then
244 if [ $PLAIN == false ]; then
245 echo "listing all notes"
246 echo ""
247 fi
248 [ $PLAIN == false ] && echo "[ID] [TITLE] [CREATED]"
249 for i in "${NOTESDIR}"/*; do
250 # shellcheck disable=SC2155
251 local fname=$(basename $i)
252 DATE=$(date -d @${fname} +"%d/%m/%Y %R %z%Z")
253 # shellcheck disable=SC2016,SC2086
254 TITLE=$($JQ -r --arg z $(basename $i) '.notes[] | select(.file == $z) | .title' $DB)
255 # shellcheck disable=SC2016,SC2086
256 ID=$($JQ -r --arg z $(basename $i) '.notes[] | select(.file == $z) | .id' $DB)
257 [ $PLAIN == false ] && echo "[${ID}] ${TITLE} ${DATE}" || echo "${ID} - ${TITLE} - ${DATE}"
258 done
259 else
260 echo "no notes yet. You can add your first one with: ${BASENAME} -a \"your note title\""
261 fi
262 }
263 function rmnote() {
264 # remove eventually existing temp DB file
265 if [[ -f $TMPDB ]]; then
266 rm $TMPDB
267 fi
268
269 NOTE=$1
270 if [ "all" == "$NOTE" ]; then
271 echo "You're going to delete all notes."
272 read -r -p "Do you wish to continue? (y/N) " ANSWER
273 case $ANSWER in
274 y|Y )
275 # shellcheck disable=SC2086
276 $JQ 'del(.notes[])' $DB > $TMPDB
277 # shellcheck disable=SC2086
278 mv $TMPDB $DB
279 # shellcheck disable=SC2086
280 rm $NOTESDIR/*
281 echo "Deleted all notes"
282 ;;
283 * )
284 echo "Aborting, no notes were deleted."
285 exit 1
286 ;;
287 esac
288 else
289 # shellcheck disable=SC2155
290 local OK=$(check_noteID "$NOTE")
291 if [ ! "$OK" ]; then
292 echo "invalid note \"$NOTE\""
293 echo "Use the note ID that you can fetch after listing your notes"
294 exit 1
295 fi
296
297 # shellcheck disable=SC2016,SC2086
298 TITLE=$($JQ --arg i $OK '.notes[] | select(.id == $i) | .title' $DB)
299 # shellcheck disable=SC2016,SC2086
300 FILE=$($JQ -r --arg i $OK '.notes[] | select(.id == $i) | .file' $DB)
301 if [ "$TITLE" ]; then
302 # shellcheck disable=SC2016,SC2086
303 $JQ -r --arg i $OK 'del(.notes[] | select(.id == $i))' $DB > $TMPDB
304 # shellcheck disable=SC2086
305 mv $TMPDB $DB
306 rm $NOTESDIR/$FILE
307 echo "Deleted note $TITLE"
308 else
309 echo "note not found"
310 exit 1
311 fi
312 fi
313 }
314 function shownote() {
315 NOTE=$1
316
317 # shellcheck disable=SC2155
318 local OK=$(check_noteID "$NOTE")
319 if [ ! "$OK" ]; then
320 echo "invalid note \"$NOTE\""
321 echo "Use the note ID that you can fetch after listing your notes"
322 exit 1
323 fi
324
325 FILE=$($JQ -r --arg i $OK '.notes[] | select(.id == $i) | .file' $DB)
326
327 if [ "$FILE" ]; then
328 $PAGER ${NOTESDIR}/${FILE}
329 fi
330 }
331 # shellcheck disable=SC2006
332 GOPT=$(getopt -o hvpla::e::d::s:: --long help,version,list,plain,userconf,backup::,add::,edit::,delete::,show:: -n 'bash-notes' -- "$@")
333
334 # shellcheck disable=SC2181
335 if [ $? != 0 ] ; then helptext >&2 ; exit 1 ; fi
336
337 # Note the quotes around `$GOPT': they are essential!
338 eval set -- "$GOPT"
339 unset GOPT
340
341 while true; do
342 case "$1" in
343 -h | --help )
344 helptext
345 exit
346 ;;
347 -v | --version )
348 echo $BASENAME v${VERSION}
349 exit
350 ;;
351 -p | --plain )
352 PLAIN=true
353 shift
354 ;;
355 -l | --list )
356 listnotes
357 exit
358 ;;
359 -a | --add )
360 case "$2" in
361 '' )
362 read -r -p "Title: " TITLE
363 ;;
364 * )
365 TITLE=$2
366 ;;
367 esac
368 shift 2
369 addnote "$TITLE"
370 exit
371 ;;
372 -e | --edit )
373 case "$2" in
374 '' )
375 read -r -p "Note ID: " NOTE
376 ;;
377 * )
378 NOTE=$2
379 ;;
380 esac
381 shift 2
382 editnote "$NOTE"
383 exit
384 ;;
385 -d | --delete )
386 case "$2" in
387 '' )
388 read -r -p "Note ID: " NOTE
389 ;;
390 * )
391 NOTE=$2
392 ;;
393 esac
394 shift 2
395 rmnote "$NOTE"
396 exit
397 ;;
398 -s | --show )
399 case "$2" in
400 '' )
401 read -r -p "Note ID: " NOTE
402 ;;
403 * )
404 NOTE=$2
405 ;;
406 esac
407 shift 2
408 shownote "$NOTE"
409 exit
410 ;;
411 --userconf )
412 export_config
413 # shellcheck disable=SC2317
414 echo "config exported to \"$RCFILE\""
415 # shellcheck disable=SC2317
416 exit
417 ;;
418 -- )
419 shift
420 break
421 ;;
422 * )
423 break
424 ;;
425 esac
426 done