#! /bin/sh
set -e

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

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

print_copyright_information () {
	cat << END
Copyright (C) 2024-2026, É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 time stamp, 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 [<TIMESTAMP>]]
   or: $PROGNAME [-d|--debug] [-f|--feuillet [<TIMESTAMP> ...]]
   or: $PROGNAME [-d|--debug] [-lm|--list-modules]
   or: $PROGNAME [-d|--debug] [-m|--module <module> [<arguments> ...]]
   or: $PROGNAME [-d|--debug] [-s|--search [<pattern> ...]]
   or: $PROGNAME [-d|--debug] [-t|--todo [<state>]|--done [<state>]]
   or: $PROGNAME [-d|--debug] [--motd|--edit-motd]

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 [<TIMESTAMP>]
			edit the entry pointed to by the time stamp in
			somewhat normalized format; if no time stamp is
			passed, it edits the register.txt file of the
			carnet.
	-f, --feuillet [<TIMESTAMP> ...]
			print out the entry pointed to by the time stamp
			in somewhat normalized format; the format is
			also highlighted to facilitate navigation
			between entries of carnet; if no time stamp is
			passed, it opens the register.txt file of the
			carnet; several time stamps can be passed, which
			will be printed out by order of apparition in
			the command arguments.
	-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, time stamps
			are highlighted for consumption by the option to
			retrieve feuillets; the command dumps the entire
			carnet in output if no pattern is given.
	-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		print out the program version.
	--version	print out the program version and license terms.
	--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.
	--motd		print out the message of the day.
	--edit-motd	edit the motd.txt file containing the message of
			the day.
	TIMESTAMP	this argument is one of the types YYYY-WW to
			select a week, or YYYY-MM-DD to select a day, or
			YYYY-MM-DDThh:mm to select a feuillet.

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.  Remark: carnet does not rely on PAGER
			and will try to make use of less or more, in
			that order.

Files:
	YYYY-WW.txt	these files, formated after the week-year and
			the week number, will store the plain text
			content of carnet entries; pretty often, those
			are the only files that are going to be present
			in a carnet directory, and that's fine.
	register.txt	this file can serve as landing pad for
			navigating through a complex carnet: it can
			store references to interesting feuillets,
			common search patterns or all kind of free form
			information that should be easy to retrieve; it
			does not have to be present if people don't feel
			the need to have one.
	motd.txt	a few lines can be dropped in this file, they
			will be printed out after the addition of an
			entry; this can be used to give and preserve the
			general orientation of the theme of the carnet;
			it is better to keep it simple and short, for
			example the vulcan idiom "live long and
			prosper"; having none is also perfectly fine.

Examples:
Main usage:
	carnet		without further ado, start editing immediately a
			new feuillet at the current time stamp; this is
			the main use of carnet: to record a note and
			give it a time stamp without having to think
			about it.
	carnet -s test	search and retrieve feuillets containing the
			character string "test"; pattern matching is
			case insensitive and the feuillet will highlight
			the matching strings.
	carnet -s sample sentence and '\\<pattern\\>'
			search and retrieve feuillets containing all the
			patterns "sample", "sentence", "and", and
			"\\<pattern\\>", which will be highlighted
			independently when the feuillet is returned by
			the carnet search function.
	carnet -r	read through carnet entries of the current week;
			this is useful to wrap up weekly activity
			reports.
Task management:
	carnet -t	search and retrieve items left to do; they are
			identified by a "[ ]" or a "TODO:" at the
			beginning of a line of the feuillet.
	carnet --done	search and retrieve items done.
	carnet -t pending
			search and retrieve items to do but in pending
			state due to not having been actionable at some
			point.
Feuillets referencing (Zettelkasten):
	carnet -f 2025-07-05T18:23
			retrieve the feuillet recorded on July 5th 2025
			at 18:23; feuillet retrieval will highlight RFC
			3339 compliant time stamps to facilitate
			navigation through feuillets that reference each
			others.
	carnet -f 2025-07-05
			retrieve all feuillets recorded on July 5th
			2025; this is useful when recalling about
			something done some day, but missing the right
			keywords to find it back.
	carnet -f 2025-27
			retrieve the entire week 27 of 2025 in the
			pager, including the contextual header; this is
			useful when wrapping up a weekly activity notice
			or timesheet.
	carnet -ef 2025-07-05T18:23
			open the text editor at the corresponding
			feuillet to bring modifications to it; this is
			useful for instance to cross reference with a
			feuillet in the future and weave further
			connections as the carnet grows.
	carnet -ef 2025-07-05
			open the text editor at the corresponding day.
	carnet -ef 2025-27
			open the text editor at the corresponding week.
	carnet -s 2025-07-05T18:23
			retrieve all the feuillets referencing the
			2025-07-05T18:23 by its RFC-style timestamp;
			this is useful to establish cross-references.
	carnet -f	display the register of the carnet; this is
			useful to document aspects of a specific carnet
			to help with navigation throughout it, like time
			stamps of useful feuillets, or documentation of
			task management statuses.
	carnet -ef	open the register of the carnet for edition.
Escape hatches:
	carnet -m hello world
			invoke an executable file called "hello" and
			localised in the carnet directory, with argument
			"world"; this is useful to implement context
			specific user custom commands.
	carnet -lm	list modules available in the carnet directory
			that can be invoked with --module; this is
			useful when dealing with a hoard of custom
			scripts.
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'
		;;
	"" | dumb )
		# Do nothing, successfully.
		true
		;;
	*)
		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"
	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 hard coded selection is on purpose, in order
# to have a controlled selection of pagers able to parse terminal escape
# controls without too much fuss.  Hard-wiring 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 ] && [ -n "$TERM" ] && [ "$TERM" != dumb ]
	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 time stamps in its standard
# output.
paint_timestamps () {
	# Neutralize painting when dealing with basic or no terminals.
	if [ -z "$TERM" ] || [ "$TERM" = dumb ]
	then
		cat
		exit
	fi
	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'
}

# match_timestamp takes a string in argument and returns 0 if it matches
# the YYYY-MM-DDThh:mm date format, otherwise it returns 1.
match_timestamp () {
	local yy_mm_ddThh_mm="$1"
	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 return 0
	else return 1
	fi
}

# match_day takes a string in argument and returns 0 if it matches the
# YYYY-MM-DD date format, otherwise it returns 1.
match_day () {
	local yy_mm_dd="$1"
	if echo "$yy_mm_dd" \
	   | grep -q '^[0-9]\{4\}-[0-1][0-9]-[0-3][0-9]$'
	then return 0
	else return 1
	fi
}

# match_week takes a string in argument and returns 0 if it matches the
# YYYY-WW week specification format, otherwise it returns 1.
match_week () {
	local yy_ww="$1"
	if echo "$yy_ww" \
	   | grep -q '^[0-9]\{4\}-[0-5][0-9]$'
	then return 0
	else return 1
	fi
}

# print_motd prints out the message of the day.
print_motd () {
	local motd="${carnet_dir}/motd.txt"
	[ ! -r "$motd" ] || cat "$motd"
}

# search_log prints entries of the carnet which match a pattern passed
# as argument.  In order to simplify the script's overall logic,
# searching is motorized by gawk.
search_log () {
	local search_pattern=""
	local highlight_pattern=""
	for search_item in "$@"
	do
		# The pattern needs preparation before being fed to gawk.
		local sanitized_search_item="$( \
			echo "$search_item" \
			| sed -z 's@/@\\/@g; s/\n/\\n/g; s/\\n$//;' \
		)"
		if [ -z "$search_pattern" ]
		then
			local search_pattern="/$sanitized_search_item/"
			local highlight_pattern="$sanitized_search_item"
			continue
		fi
		local search_pattern="${search_pattern} && /${sanitized_search_item}/"
		local highlight_pattern="${highlight_pattern}|${sanitized_search_item}"
	done
	cd "$carnet_dir"


	# Highlight matching search patterns to facilitate research.
	if [ -n "$TERM" ] && [ "$TERM" != "dumb" ]
	then
		local highlight="$(tput bold)$(tput setaf 1)"
		local normal="$(terminfo_normal)"
	else
		local highlight=
		local normal=
	fi

	# 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;
		}

		('"$search_pattern"') {
			ENTRY = $0;
			sub("\n\n", " : ", HEAD);
			# Colorize output
			gsub( \
				/'"$highlight_pattern"'/, \
				"'"$highlight"'&'"$normal"'", \
				ENTRY \
			);
			# Normalize time stamp 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 () {
	# Check that mpc is available and can connect to mpd before use.
	if type mpc >/dev/null 2>&1 && mpc status >/dev/null 2>&1
	then
		local on_air="$( \
			mpc status --format '[[%artist% - ]%title%]|[%file%]' \
			| sed -n 1p \
		)"
		if [ "${on_air% n/a  *}" = "volume:" ]
		then local on_air=
		fi
	fi
	if [ -z "$on_air" ] && type mocp >/dev/null 2>&1
	then
		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.
		local on_air="${on_air# - }"
	fi
	if [ -n "$on_air" ]
	then local on_air=" on air: $on_air"
	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 time stamp 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"
	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="print FILENAME\":\"NR ;exit 0;"
	else local extra_found_instructions="print \"$SEPARATOR\";"
	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;
				'"$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"
}

# locate_day takes a date in format YYYY-MM-DD and outputs the
# corresponding day 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_day () {
	local yy_mm_dd="$1"
	local only_location="$2"
	local week="$(date -d"$yy_mm_dd" +"%G-%V")"
	local weekfile="$carnet_dir/${week}.txt"
	if [ "$only_location" = "only_location" ]
	then local extra_found_instructions="print FILENAME\":\"NR ;exit 0;"
	else local extra_found_instructions="print \"$SEPARATOR\";"
	fi
	gawk '
		BEGIN {
			inday=0;
			found=0;
		}

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

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

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

# locate_week takes a date in format YYYY-WW and outputs the
# corresponding week 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 of the entry, useful for passing as argument of a text
# editor.
locate_week () {
	local week="$1"
	local only_location="$2"
	local weekfile="$carnet_dir/${week}.txt"
	if [ "$only_location" = "only_location" ]
	then echo "${weekfile}:1"
	else cat "$weekfile"
	fi
}

# 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 an
# item to do 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 an item to do that has been done, 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"
	local no_warn=true
	if [ ! -r "$carnet_dir/register.txt" ] && [ -z "$timestamp" ]
	then local no_warn=false
	fi
	if [ -z "$timestamp" ]
	then
		setup_locks "$filename"
		"$VISUAL" "$carnet_dir/register.txt"
		"$no_warn" || cat <<-END>&2
		warning: there were no time stamp provided to --feuillet,
		         and no entries of interest were recorded in:
		         $carnet_dir/register.txt
		         so carnet started a blank register.
		END
		exit
	else
		local entrypoint=
		if match_timestamp "$timestamp"
		then local entrypoint="$(locate_feuillet "$timestamp" only_location)"
		elif match_day "$timestamp"
		then local entrypoint="$(locate_day "$timestamp" only_location)"
		elif match_week "$timestamp"
		then local entrypoint="$(locate_week "$timestamp" only_location)"
		else
			cat <<-END >&2
			error: $timestamp is a malformed entry reference.
			       Please use one of:
			         * YYYY-MM-DDThh:mm to choose a feuillet,
			         * YYYY-MM-DD to choose a date,
			         * YYYY-WW to choose a week.
			END
			exit 1
		fi
		local filename="${entrypoint%:*}"
		local lineno="${entrypoint##*:}"
		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 () {
	set_less_options
	# No option were passed.
	if [ -z "$*" ] && [ -r "$carnet_dir/register.txt" ]
	then
		cat "$carnet_dir/register.txt" \
		| paint_timestamps \
		| read_with_pager
	elif [ -z "$*" ] && [ ! -r "$carnet_dir/register.txt" ]
	then
		cat <<-END>&2
		error: there were no time stamp provided to --feuillet,
		       and no entries of interest are recorded in:
		       $carnet_dir/register.txt
		END
	fi
	for timestamp in "$@"
	do
		if match_timestamp "$timestamp"
		then locate_feuillet "$timestamp"
		elif match_day "$timestamp"
		then locate_day "$timestamp"
		elif match_week "$timestamp"
		then locate_week "$timestamp"
		else
			cat <<-END >&2
			error: $timestamp is a malformed entry reference.
			       Please use one of:
			         * YYYY-MM-DDThh:mm to choose a feuillet,
			         * YYYY-MM-DD to choose a date,
			         * YYYY-WW to choose a week.
			END
			exit 1
		fi
	done \
	| paint_timestamps \
	| read_with_pager
}

# handle_edit_motd opens the motd.txt file in the text editor.
handle_edit_motd () {
	"$VISUAL" "$carnet_dir/motd.txt"
}

# 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 "$@"
		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
	;;
	--motd)
		shift
		print_motd
		exit
	;;
	--edit-motd)
		shift
		handle_edit_motd
		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" time stamp 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 simple 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 time stamp, 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
	else
		# Or this new entry can be the first one of the day.
		cat >> "$entry" <<-END

		${SEPARATOR}
		${datestamp}

		${time} :${trailing_context}

		END
	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
fi

# 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"

# Trailing information after file edition.
print_motd
printf 'feuillet %s recorded in %s.\n' "$year-$month-${day}T$time" "$entry"
