#! /bin/sh
set -e

readonly PROGNAME="${0##*/}"
readonly VERSION="1.7.0"

print_version () {
	cat << END
${PROGNAME} ${VERSION}
END
}

print_copyright_information () {
	cat << END
Copyright (C) 2024-2025, Étienne Mollier <emollier@emlwks999.eu>

This program is free software: you can redistribute it and/or modify it
under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or (at
your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.
END
}

print_description () {
	cat << END
${PROGNAME} is a logbook and note taking helper.  It provides a way to
record seamlessly journal entries, along automated recording of the time
and date and all sorts of interesting information, like recorded
calendar(1) activities, or what could be playing on music players.
Invoked without arguments, ${PROGNAME} opens straight the log book to
create a new entry at the current time and date at the tail of the file.
Use cases include, but are not limited to: scratch pad for taking notes,
timesheet recording, or notes storage and retrieval facility; the killer
feature is that it may be all at once as the needs of the writer evolve
over time.

The "carnet" log book is a set of text files, one per week, made of
entries named "feuillets" separated by a timestamp, and specific markers
to separate days of the week; this avoids exploding the file size of a
long running log file typed for a while by a proficient writer, while
also avoiding the explosion of file numbers that could appear with
compulsive writers.

END
}

print_usage () {
	cat << END
Usage: $PROGNAME [-d|--debug] [-a|--activity] [-r|--readonly]
   or: $PROGNAME [-d|--debug] [-h] [--help] [-v|--version]
   or: $PROGNAME [-d|--debug] [-ef|--edit-feuillet [<YYYY-MM-DDThh:mm>]]
   or: $PROGNAME [-d|--debug] [-f|--feuillet [<YYYY-MM-DDThh:mm>]]
   or: $PROGNAME [-d|--debug] [-m|--module <module> [<arguments> ...]]
   or: $PROGNAME [-d|--debug] [-s|--search [<pattern> ...]]
   or: $PROGNAME [-d|--debug] [-t|--todo [<state>]]
   or: $PROGNAME [-d|--debug] [--done [<state>]]

END
}

print_help () {
	cat << END
Options:
	-a, --activity   print out a histogram of activity for each
	                 week.
	-d, --debug      print out debug information upon invocation.
	-ef, --edit-feuillet [<YYYY-MM-DDThh:mm>]
	                 edit the entry pointed to by the timestamp in
	                 simili-normalized format; if no timestamp is
	                 passed, it edits the register.txt file of the
	                 carnet.
	-f, --feuillet [<YYYY-MM-DDThh:mm>]
	                 print out the entry pointed to by the timestamp
	                 in simili-normalized format; the format is also
	                 highlighted to facilitate navigation between
	                 entries of carnet; if no timestamp is passed,
	                 it opens the register.txt file of the carnet.
	-h               print out the short usage notice.
	--help           print out the full description, usage and help
	                 message.
	-l, --list-modules
	                 list module programs shipped within a given
	                 carnet; see --module for modules purpose.
	-m, --module <module> [<argument> ...]
	                 execute a module program shipped within a given
	                 carnet; this is intended for power users as an
	                 escape hatch to e.g. implement customized
	                 search engines, or specific activity metrics.
	-r, --readonly   look up the carnet in read-only mode, without
	                 appending a new entry.
	-s, --search [<pattern> ...]
	                 look up log book entries matching the provided
	                 search patterns; as a convenience, timestamps
	                 are highlighted for consumption by the option
	                 to retrieve feuillets; the command dumps the
	                 entire carnet in output if no pattern is given;
	                 multiple patterns will be treated as or'ed.
	-t, --todo [<state>]
	                 list "todo" items found in the carnet; todo
	                 items can be materialized using a sort of
	                 square materialized by brackets like '[ ]' at
	                 the beginning of a line; the blank space may be
	                 later erased to materialize the new state of
	                 the task, e.g. '[*]' or '[DONE]' at the
	                 discretion of the writer.  The optional state
	                 argument may be used to list tasks materialized
	                 with a given state, again mostly free form at
	                 the discretion of the writer.
	-v, --version    print out the program version.
	--done [<state>]
	                 shortcut for common markers representing "todo"
	                 items in the state done, primarily provided as
	                 a convenience to match "[*]" and other common
	                 patterns marking tasks completed.

Environment:
	CARNET_DIR       indicates the directory in which the carnet
	                 content is stored.  The default is to put it in
	                 the carnet/ directory within the user's default
	                 "Documents" directory.
	CARNET_EDITOR    overrides EDITOR and VISUAL environment
	                 variables settings.  The default is unset.
	CARNET_LANG      overrides LANG environment variable.  This is
	                 unset by default.
	CARNET_LESS      overrides LESS environment variable.  This is
	                 unset by default.
	CARNET_OVERTIME  indicate after how many hours work on the
	                 carnet is considered overtime for the day;
	                 single integer values will express hours,
	                 otherwise durations may be expressed with the
	                 hh:mm format;
	                 unset or zero value will disable the option.
	CARNET_PAGER     provide a custom pager.  This is unset by
	                 default.  Contrary to previous carnet versions,
	                 this does not override PAGER, which is now
	                 unused.
END
}

readonly SEPARATOR="$(for i in $(seq 72); do printf _ ; done ; printf '\n')"

# carnet may use localised time and dates in a language independent of
# the user's, hence providing an environment variable to set a specific
# one if requested.
if [ -n "$CARNET_LANG" ]
then
	LANG="$CARNET_LANG"
	export LANG
fi

# The carnet_dir indicates where to look for the entries.
if [ -n "$CARNET_DIR" ]
then
	readonly carnet_dir="${CARNET_DIR%%/}"
else
	if [ "$USER" = "root" ]
	then
		# Not necessarily wanting to encourage the use of carnet
		# as root, but it seems perfectly legitimate to wish to
		# record a host associated carnet maintained by a team
		# of system administrator.  For such a scenario, putting
		# the carnet along the rest of the system logs feels
		# kind of appropriate by default.  This can be
		# overridden by the CARNET_DIR environment in any case.
		readonly carnet_dir="/var/log/carnet"
	elif type xdg-user-dir > /dev/null
	then
		readonly xdg_doc_dir="$(xdg-user-dir DOCUMENTS)"
		readonly carnet_dir="${xdg_doc_dir%%/}/carnet"
	else
		readonly carnet_dir="${HOME%%/}/Documents/carnet"
	fi
fi

if [ -n "$CARNET_EDITOR" ]
then
	VISUAL="$CARNET_EDITOR"
	export VISUAL
elif [ -z "$VISUAL" ] && [ -n "$EDITOR" ]
then
	VISUAL="$EDITOR"
	export VISUAL
elif [ -z "$VISUAL" ]
then
	VISUAL=vi
	export VISUAL
fi

# terminfo_normal output the escape sequence to restore normal font
# depending on the terminal type.
terminfo_normal () {
	case "$TERM" in
	# sgr0 has crufty output on certain terminals.
	linux | screen* | tmux*) printf -- '\e[m' ;;
	*)      tput sgr0 ;;
	esac
}

# setup_locks will check for the existence of carnet lock files for the
# text file given in argument, eventually editor swap files when
# possible, and set locks before modifying entries by the carnet, or
# aborts the script failing to do so.
#
# WARNING: this function sets the LOCKFILE global variable and a trap on
# EXIT, so it can be invoked at most once during a carnet invocation.
setup_locks () {
	local filename="$1"
	readonly LOCKFILE="$(dirname "$filename")/.${filename##*/}.carnet.lock"
	# Abort in case of detection of certain editors posing locks.
	local vi_type_lock="$(dirname "$filename")/.${filename##*/}.swp"
	# FIXME: handling other editors would be much appreciated.
	if [ -e "$LOCKFILE" ] || [ -e "$vi_type_lock" ]
	then
		cat >&2 <<-END
		error: the following entries look to be under edition:
		       $filename
		END
		if [ -r "$LOCKFILE" ]
		then
			local pid="$(cat "$LOCKFILE")"
			local user="$( \
				ps aux \
				| sed -n 's/^\([^ ]*\) '"$pid"' .*/\1/p'
			)"
			printf '       carnet has pid %s' "$pid" >&2
			if [ -n "$user" ]
			then printf ', owner: %s.\n' "$user" >&2
			else printf '.\n' >&2
			fi
		else
			printf '       They seem opened outside carnet.\n'
		fi
		exit 1
	fi
	# Apply the lock file
	echo "$$" > "$LOCKFILE"
	# Make sure the lock file has the expected content.  The compare
	# and swap is not atomic, but this is much better than no checks.
	if [ "$$" != "$(cat "$LOCKFILE")" ]
	then
		cat >&2 <<-END
		error: carnet raced against pid $(cat "$LOCKFILE").
		END
		exit 1
	fi
	trap 'rm "$LOCKFILE"' EXIT
}

# read_with_pager reads its standard input using one of less, more or
# cat, in that order.  The harcoded selection is on purpose, in order to
# have a controlled selection of pagers able to parse terminal escape
# controls without too much fuss.  Hardwiring the reader may be
# reconsidered when more comfortable with the handling of a large
# selection of pagers.  Execution of the pager is on purpose, we always
# want the pager to be the final command of carnet execution.
read_with_pager () {
	if [ -t 1 ]
	then
		if [ -n "$CARNET_PAGER" ] \
		   && type "$CARNET_PAGER" > /dev/null
		then exec "$CARNET_PAGER"
		elif type less > /dev/null
		then exec less -R
		elif type more > /dev/null
		then exec more
		else exec cat
		fi
	else
		exec cat
	fi
}

# set_less_options does what is written on the can.  If neither
# environment variables LESS or CARNET_LESS are set, then offer a sane
# default value to LESS.
set_less_options () {
	type less > /dev/null 2>&1 || return 0
	if [ -n "$CARNET_LESS" ]
	then
		LESS="$CARNET_LESS"
		export LESS
	fi
	test -z "$LESS" || return 0
	local less_version="$( \
		less --version \
		| sed -n '1s/less \([0-9]\+\).*/\1/p'
	)"
	if [ "$less_version" -ge 668 ]
	then
		case "$TERM" in
		xterm* | foot* | tmux* | screen*)
			LESS="FRi--redraw-on-quit"
		;;
		*)
			LESS="FRi"
		;;
		esac
	else
		LESS="FRi"
	fi
	export LESS
}

# lineno_argument takes the name of a text editor in argument and a line
# number as a second argument.  It echoes the character string
# corresponding to the argument which is needed for the given text
# editor to start at a certain line number.
lineno_argument () {
	local editor="$1"
	local lineno="$2"
	case "$editor" in
	kate | kwrite | mousepad )
		# This line setting mechanism is compatible with KDE and
		# Xfce editors.
		printf -- '%s' "--line=$lineno"
	;;
	* )
		# This mechanism works with a range of text editors such
		# as vi, emacs or nano.
		printf -- '%s' "+$lineno"
	;;
	esac
}

# paint_timestamps takes no arguments.  It reads its standard input for
# text and highlights all occurrences of timestamps in its standard
# output.
paint_timestamps () {
	local ts_highlight="$(tput bold)$(tput setaf 4)"
	local normal="$(terminfo_normal)"
	sed 's/\([0-9]\{4\}-[0-1][0-9]-[0-3][0-9]T[0-2][0-9]:[0-5][0-9]\)/'"$ts_highlight"'\1'"$normal"'/g'
}

# search_log prints entries of the carnet which match a pattern passed
# as argument.  In order to simplify the script's overally logic,
# searching is motorized by gawk.
search_log () {
	local pattern=""
	for search_item in "$@"
	do
		if [ -z "$pattern" ]
		then
			local pattern="$search_item"
			continue
		fi
		local pattern="$pattern|$search_item"
	done
	cd "$carnet_dir"

	# The pattern needs preparation before being fed to gawk.
	local sanitized_pattern="$( echo "$pattern" | sed 's@/@\\/@g' )"

	# Highlight matching search patterns to facilitate research.
	local highlight="$(tput bold)$(tput setaf 1)"
	local normal="$(terminfo_normal)"

	# The research proper is done by an extensive gawk script.
	gawk '
		BEGIN{
			RS="\n\n'"$SEPARATOR"'\n|\n\n([0-2][0-9]:[0-6][0-9] :[^\n]*)";
			ORS="\n\n'"$SEPARATOR"'\n";
			IGNORECASE=1;
			indate = 0;
			matching_feuillets = 0;
		}

		/^Date :/ {
			indate = 1;
			DATE = $0;
			sub("Date", "", DATE);
		}

		(FNR==1 || indate==1) {
			indate = 0;
			HEAD = RT;
			sub(".*\n\n([0-2][0-9]:[0-6][0-9] :)", "&", HEAD)
			next;
		}

		/'"$sanitized_pattern"'/ {
			ENTRY = $0;
			sub("\n\n", " : ", HEAD);
			# Colorize output
			gsub( \
				/'"$sanitized_pattern"'/, \
				"'"$highlight"'&'"$normal"'", \
				ENTRY \
			);
			# Normalize timestamp to facilitate reuse as feuillet.
			sub(/\)$/, "", DATE);
			sub(/ ?: /, "T", HEAD);
			sub(/ :/, ") :", HEAD);
			OUTPUT = FILENAME DATE HEAD TIMESTAMP ENTRY;
			print OUTPUT;
			matching_feuillets += 1;
		}

		{
			HEAD = RT;
		}

		END{
			ORS="\n";
			if (matching_feuillets == 1) {
				print "1 feuillet found.";
				exit;
			}
			print matching_feuillets, "feuillets found.";
		}
	' \
		[0-9][0-9][0-9][0-9]-[0-5][0-9].txt
}

# show_music_on_air captures the music playing at t time and prints it
# to standard output.  If nothing plays or if no player is available at
# all, then it simply outputs nothing.
show_music_on_air () {
	type mocp >/dev/null 2>&1 || return
	# FIXME: modularize to handle other music players.
	local on_air="$(mocp -Q '%artist - %song' 2>/dev/null ||:)"
	# Trim leading " - " if any, this comes from radio stations not
	# able to differentiate authors and titles in their metadata
	# stream.
	on_air="${on_air# - }"
	if [ -n "$on_air" ]
	then on_air=" on air: $on_air"
	# FIXME: internationalize and localize.
	fi
	echo "$on_air"
}

# calculate_overtime takes the CARNET_OVERTIME value and tries to
# estimate whether too much time has been spent since the first entry of
# the day has been written.  If CARNET_OVERTIME is unset or 0 hour, the
# functionality is ignored, otherwise the integer value stored in the
# environment variable will store the duration in hours tolerated
# between the current entry and the first entry of the day.
calculate_overtime () {
	# First check whether it makes sense to calculate overtime.
	case "$CARNET_OVERTIME" in
	'' | '0' | '0:0' | '0:00' | '00:00' ) return ;;
	esac
	# Then we begin to capture arguments of calculate_overtime.
	local yyyy_mm_dd="$1"
	local hh_mm="$2"
	local file="$3"
	local overtime_hh="${CARNET_OVERTIME%:*}"
	local overtime_h="${overtime_hh#0}"
	if [ "${CARNET_OVERTIME#*:}" != "${CARNET_OVERTIME}" ]
	then
		local overtime_mm_partial="${CARNET_OVERTIME#*:}"
		local overtime_m_partial="${overtime_mm_partial#0}"
	else
		local overtime_m_partial=0
	fi
	# This is the total overtime in minutes
	local overtime_m="$((60 * overtime_h + overtime_m_partial))"
	# Check whether we already have an entry today: if so, obtain
	# the time, or nothing if not.
	test -r "$file" || return
	local start_hh_mm="$(
		gawk '/^Date : .* \('"$yyyy_mm_dd"'\)$/ {
			getline;
			getline header;
			split(header, Time, / :/);
			print Time[1];
		}' \
		"$file"
	)"
	test -n "$start_hh_mm" || return
	# Slice hh:mm to separate the two numbers.
	local start_hh="${start_hh_mm%:*}"
	# Trim potential leading 0 tripping octal calculations.
	local start_h="${start_hh#0}"
	# Same dance for the current hour.
	local hh="${hh_mm%:*}"
	local h="${hh#0}"
	# Calculate the durations; minutes can be negative.
	local duration_h="$((h - start_h))"
	# Same dance as the hours above, but for minutes.
	local start_mm="${start_hh_mm#*:}"
	local start_m="${start_mm#0}"
	local mm="${hh_mm#*:}"
	local m="${mm#0}"
	# This is the total duration in minutes, not just the relative one.
	local duration_m="$((60 * duration_h + m - start_m))"
	if [ "$duration_m" -le "$((overtime_m - 30))" ]
	then
		# Still time to work
		return
	elif [ "$duration_m" -le "$overtime_m" ]
	then
		local rest="$((overtime_m - duration_m))"
		echo " day is ending: still ${rest}m to go"
	elif [ "$duration_m" -le "$((overtime_m + 30))" ]
	then
		local trail="$((duration_m - overtime_m))"
		echo " warning: overtime for ${trail}m."
	else
		echo " WARNING: EXCESSIVE OVERTIME"
	fi
}

# list_modules does what is written on the can.  It looks up the carnet
# directory and lists executable files that can be invoked to be run as
# modules.
list_modules () {
	local file
	for file in "$carnet_dir"/*
	do
		if [ -x "$file" ]
		then echo "${file#${carnet_dir}/}"
		fi
	done
}

# run_module runs modules associated to a carnet, that is, an executable
# stored in the carnet_dir.  This may be useful to power users after
# very specific capabilities, or to prototype new functionalities.
run_module () {
	local module="$1"
	if [ -z "$module" ]
	then
		cat >&2 <<-END
		error: please specify the carnet module name.
		END
		exit 1
	fi
	shift
	if [ ! -x "$carnet_dir/$module" ]
	then
		cat >&2 <<-END
		error: the module $module does not exist in carnet directory:
		       $carnet_dir
		       or it exists but is not executable.
		END
		exit 1
	fi
	# If this is not shipped via environment, then there is no
	# obvious way for the module to get the carnet_dir location.
	CARNET_DIR="$carnet_dir"
	export CARNET_DIR
	exec "$carnet_dir/$module" "$@"
}

# locate_feuillet takes a timestamp in format YYYY-MM-DDThh:mm and
# outputs the corresponding entry of the carnet if one is found,
# otherwise it outputs nothing and returns an error code.  It takes an
# optional second argument "only_location" to instruct the function to
# only return the file name and line number of the entry, useful for
# passing as argument of a text editor.
locate_feuillet () {
	local yy_mm_ddThh_mm="$1"
	local only_location="$2"
	if ! echo "$yy_mm_ddThh_mm" \
	   | grep -q '[0-9]\{4\}-[0-1][0-9]-[0-3][0-9]T[0-2][0-9]:[0-5][0-9]'
	then
		cat <<-END >&2
		error: $yy_mm_ddThh_mm is a malformed entry reference.
		       Please use format YYYY-MM-DDThh:mm.
		END
		exit 1
	fi
	local week="$(date -d"$yy_mm_ddThh_mm" +"%G-%V")"
	local weekfile="$carnet_dir/${week}.txt"
	local yy_mm_dd="$(echo "$yy_mm_ddThh_mm" | cut -f1 -dT)"
	local hh_mm="$(   echo "$yy_mm_ddThh_mm" | cut -f2 -dT)"
	if [ "$only_location" = "only_location" ]
	then local extra_found_instructions="exit 0;"
	else local extra_found_instructions=""
	fi
	gawk '
		BEGIN {
			inday=0;
			intime=0;
			found=0;
		}

		/^Date ?: [^ ]* \([^)]*\)$/ {
			inday=0;
		}

		/^[0-2][0-9]:[0-5][0-9] ?:/ {
			intime=0;
		}

		/^Date ?: [^ ]* \('"$yy_mm_dd"'\)$/ {
			inday=1;
		}

		/^'"$hh_mm"' ?:/ {
			if (inday) {
				intime=1;
				found=1;
				print FILENAME":"NR ;
				'"$extra_found_instructions"'
			}
		}

		(intime && inday) {
			if (/^'"$SEPARATOR"'$/) exit;
			print;
		}

		END {
			if (!found) {
				print "error: no entry on '"$yy_mm_ddThh_mm"'." \
				> "/dev/stderr";
				exit 1;
			}
		}
	' "$weekfile"
}

# handle_search takes a regex to search for entries and outputs all
# feuillets having expressions matching the argument.
handle_search () {
	# When no pattern is specified, match all entries without
	# excessive cluttering of the output with highlight escape
	# sequences.
	set_less_options
	search_log "${@:-^}" \
	| paint_timestamps \
	| read_with_pager
}

# handle_todo_items takes no argument and outputs all entries having
# leftover items identified as still to be done.  The convention for a
# todo item is to materialise it as an opening bracket, one or more
# spaces and a closing bracket; the idea is to resolve the item once
# done by filling it with  mark is deemed the most appropriate
# by the writer.  If an argument is passed anyways, then it is used to
# materialize the state of a todo item that has been donw, or cancelled,
# or is pending, or whichever state can be imagined for a task.
handle_todo_items () {
	local state="$1"
	set_less_options
	if test -z "$state"
	then search_log '\\n\[  *\]|\\nTODO[[:space:]]?: |\\nFIXME[[:space:]]?: '
	else search_log '\\n\[ *'"$state"' *\]'
	fi \
	| paint_timestamps \
	| read_with_pager
}

# handle_done_items is mostly equivalent to handle_todo_items but
# facilitates fetching resolved items without further ado.
handle_done_items () {
	local state="$1"
	set_less_options
	if test -z "$state"
	then search_log '\\n\[ *([*]|done|x|✓) *\]|\\nDONE[[:space:]]?: '
	else search_log '\\n\[ *'"$state"' *\]'
	fi \
	| paint_timestamps \
	| read_with_pager
}

# handle_activity is a function taking no arguments.  It is designed to
# describe accurately the behavior of the carnet option --activity.
handle_activity () {
	# FIXME: move side executables to a libexec directory.
	if [ -x ./carnet_activity.pl ]
	then
		# We're in development directory context.
		exec ./carnet_activity.pl "$carnet_dir"
	elif type carnet_activity.pl > /dev/null 2>&1
	then
		# We're in "production" deployment context.
		exec carnet_activity.pl "$carnet_dir"
	else
		cat <<-END >&2
		error: missing carnet_activity.pl to process statistics.
		       This file is supposed to be part of the source
		       code and the installation.
		END
		exit 1
	fi
}

# handle_list_modules is a function taking no arguments.  It is designed
# to describe the behavior of the carnet option --list-modules.
handle_list_modules () {
	if [ -t 1 ]
	then
		list_modules \
		| column \
		| read_with_pager
	else
		list_modules
	fi
}

# handle_edit_feuillet is a function taking a string in format
# YYYY-MM-DDThh:mm in order to open the carnet at the corresponding
# feuillet.  It is designed to describe accurately the behavior of the
# carnet command option --edit-feuillet.
handle_edit_feuillet () {
	local timestamp="$1"
	if [ ! -r "$carnet_dir/register.txt" ] && [ -z "$timestamp" ]
	then
		cat <<-END>&2
		warning: there were no timestamp provided to --feuillet,
		         and no entries of interest are recorded in:
		         $carnet_dir/register.txt
		         register.txt will open for edition in 5 seconds…
		END
		sleep 5
	fi
	if [ -z "$timestamp" ]
	then
		setup_locks "$filename"
		"$VISUAL" "$carnet_dir/register.txt"
		exit
	else
		local feuillet_location="$(locate_feuillet "$1" only_location)"
		local filename="${feuillet_location%:*}"
		local lineno="${feuillet_location##*:}"
		setup_locks "$filename"
		"$VISUAL" \
			"$(lineno_argument "$VISUAL" "$lineno")" \
			"$filename"
		exit
	fi
}

# handle_feuillet is a function taking a string in format
# YYYY-MM-DDThh:mm in order to print out the corresponding feuillet.  It
# is designed to describe accurately the behavior of the carnet command
# option --feuillet.
handle_feuillet () {
	local timestamp="$1"
	set_less_options
	if [ -z "$timestamp" ] && [ -r "$carnet_dir/register.txt" ]
	then
		cat "$carnet_dir/register.txt" \
		| paint_timestamps \
		| read_with_pager
	elif [ -z "$timestamp" ] && [ ! -r "$carnet_dir/register.txt" ]
	then
		cat <<-END>&2
		error: there were no timestamp provided to --feuillet,
		       and no entries of interest are recorded in:
		       $carnet_dir/register.txt
		END
	else
		locate_feuillet "$1" \
		| paint_timestamps \
		| read_with_pager
	fi
}

# handle_short_help assembles the help message when carnet is invoked
# with the shorthand -h argument.  The idea is to have a help message
# short enough that it fits within any reasonably sized terminal window,
# i.e. 72 columns by 24 lines.
handle_short_help () {
	print_usage
	cat << END
For extended description, please read the carnet(1) manual page, or run:

	\$ carnet --help
END
}

# handle_help assembles the help message when carnet is invoked with the
# long --help argument.  The idea is to provide extended help when
# manual pages are not an option; the function is also used for
# bootstrapping the manual page with help2man.
handle_help () {
	if [ -t 1 ]
	then
		{
		print_description
		print_usage
		print_help
		} | read_with_pager
	else
		print_description
		print_usage
		print_help
	fi
}

# Default behavior is to add a new entry to the carnet, not to look it
# up, but this variable may be erased when decoding command arguments.
readonly_journal='false'
while [ -n "$1" ]
do
	case "$1" in
	-a|--activity)
		shift
		handle_activity
		exit
	;;
	-d|--debug)
		shift
		set -x
	;;
	-ef|--edit-feuillet)
		shift
		handle_edit_feuillet "$1"
		exit
	;;
	-f|--feuillet)
		shift
		handle_feuillet "$1"
		exit
	;;
	-h)
		shift
		handle_short_help
		exit 0
	;;
	--help)
		shift
		handle_help
		exit 0
	;;
	-lm|--list-module|--list-modules)
		shift
		handle_list_modules
		exit 0
	;;
	-m|--module)
		shift
		run_module "$@"
		exit
	;;
	-r|--readonly)
		shift
		readonly_journal='true'
	;;
	-s|--search)
		shift
		handle_search "$@"
		exit
	;;
	-t|--todo)
		shift
		handle_todo_items "$1"
		exit
	;;
	--done)
		shift
		handle_done_items "$1"
		exit
	;;
	-v)
		shift
		print_version
		exit 0
	;;
	--version)
		shift
		print_version
		print_copyright_information
		exit 0
	;;
	*)
		cat <<-END >&2
		error: $1: unknown argument.
		       Pass --help for information how to use $PROGNAME.
		END
		exit 1
	;;
	esac
done
readonly readonly_journal

# date is recorded only once, in order to avoid inconsistencies, if for
# example the script starts up slightly before midnight and finishes
# slightly after : state is preserved only at the beginning of the
# script and in a single pass, and slices the character string only
# there after as needed.
readonly date="$( date +'%F-%T%z;%G-%V;%A' )"

# But there is the special case of obtaining the past and next weeks,
# which date does not allow acquiring in a single pass.
readonly last_week="$( date -d'last week' '+%G-%V' )"
readonly next_week="$( date -d'next week' '+%G-%V' )"

# Slicing of the "now" timestamp starts here.
readonly year="$(      echo "$date" | cut -f1 -d-   )"
readonly month="$(     echo "$date" | cut -f2 -d-   )"
readonly day="$(       echo "$date" | cut -f3 -d-   )"
readonly time="$(      echo "$date" | cut -f4 -d- | cut -f1-2 -d: )"
readonly weekyear="$(  echo "$date" | cut -f2 -d';' | cut -f1 -d- )"
readonly week="$(      echo "$date" | cut -f2 -d';' | cut -f2 -d- )"
readonly dayofweek="$( echo "$date" | cut -f3 -d';' )"

# There's no simpe way to return the last Monday, including when such
# Monday is today, by using date, hence the slightly complicate logic
# here.  Due to usage of at least month and day, this logic must be
# applied after the slicing.  The local_monday is guaranteed to be the
# Monday localised in the language of the carnet, no matter the point in
# time in which date is executed.
readonly local_monday="$(date -d'Monday' +%A)"
readonly latest_sunday="$( \
	if [ "$dayofweek" = "$local_monday" ]
	then echo "$month$day"
	else date -d'last Monday' +'%m%d'
	fi
)"

# entry is the file of the week in which to record the new carnet entry.
readonly entry="${carnet_dir}/${weekyear}-${week}.txt"
readonly last_entry="${last_week}.txt"
readonly next_entry="${next_week}.txt"
readonly first_entry="$(ls -1 ${carnet_dir} | sed -n 1p)"
readonly datestamp="Date : $dayofweek (${year}-${month}-${day})"

# From now on, there is enough information to open the carnet read-only
# at the current week.  We want the script to end now as later code
# deals with opening the carnet specifically for writing.
if "$readonly_journal"
then
	set_less_options
	cat "$entry" \
	| paint_timestamps \
	| read_with_pager
	exit
fi

# Calculate the overtime alert if applicable; this depends on the
# setting of environment variable CARNET_OVERTIME.
readonly overtime_alert="$(
	calculate_overtime "${year}-${month}-${day}" "$time" "$entry"
)"

# Prepare the trailing context appearing after the timestamp, if
# anything relevant is of interest to see appear.  This shows the music
# airing, but will prioritize overtime alerts if applicable.
readonly trailing_context="$( \
	if [ "$USER" = "root" ] && [ -n "$SUDO_USER" ]
	then echo " from $SUDO_USER"
	elif test -n "$overtime_alert"
	then echo "$overtime_alert"
	else show_music_on_air
	fi
)"

mkdir -p "$carnet_dir"
setup_locks "$entry"

if [ -e "$entry" ]
then
	# This is the most common situation, when adding a new entry in
	# an existing carnet file.
	if grep -q "^${datestamp}\$" "$entry"
	then
		# This new entry can be added to the ongoing day.
		cat >> "$entry" <<-END

		${time} :${trailing_context}

		END
		# FIXME: internationalize space before colon.
	else
		# Or this new entry can be the first one of the day.
		cat >> "$entry" <<-END

		${SEPARATOR}
		${datestamp}

		${time} :${trailing_context}

		END
		# FIXME: internationalize space before colon.
	fi
else
	# Otherwise we are in the situation where a new carnet file
	# needs to be issued, as the previous week has ended.
	cat > "$entry" <<-END
	${last_entry} - previous week
	${next_entry} - next week
	${first_entry} - first week

	carnet logbook, year ${weekyear}, week ${week}

	END

	# When available, calendar(1) enhances the carnet by summarizing
	# the activity of the upcoming week.
	if type calendar > /dev/null 2>&1
	then
		cat >> "$entry" <<-END
		On schedule:
		$(LANG=C.UTF-8 calendar -A6 -t "$latest_sunday")

		END
	fi

	cat >> "$entry" <<-END
	${SEPARATOR}
	${datestamp}

	${time} :${trailing_context}

	END
	# FIXME: internationalise the header entry.
fi

printf 'note: carnet recorded in %s.\n' "$entry"

# Now editing the current entry by starting at the last line of the file
# straight away.
readonly LINENO="$(wc -l "$entry"  | cut -f 1 -d ' ')"
"$VISUAL" "$(lineno_argument "$VISUAL" "$LINENO")" "$entry"
