#!/bin/sh
# Copyright 2018 Didier Spaier <didier~at~slint~dot~fr>
#
# All rights reserved.
#
# Redistribution and use of this script, with or without modification, is
# permitted provided that the following conditions are met:
#
# 1. Redistributions of this script must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
#
#  THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
#  WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
#  MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO
#  EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
#  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
#  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
#  OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
#  WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
#  OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
#  ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

# Updated on Thu, 12 Jul 2018 00:42:09 +0200
# Add as "super-fallback" /EFI/startup.nsh if absent?
# Internationalize.
# Provide a pre-built EFI image
# Ship in an initramfs to make a real rescue stick.
# Use a rEFInd EFI image instead of a GRUB one.
# Allow to use an already formatted USB stick.
# Do not accept to write in the already mounted ESP of an USB stick and
# warn the user then.

# This script provides boot menu entries for all accessible EFI OS
# loaders.

# Features:
# Write an EFI application displaying an menu entry for each EFI boot
# loader located in a device connected to the computer.
# Install the EFI application in a mass storage device, in a specific
# location and optionally as a fall back as /BOOT/EFI/BOOTx64.EFI
# Install the EFI application in an USB stick.
# Make an entry for the EFI application menu in the firmware.
# Allow the user to hide menu entries, edit and change the order of
# their displayed labels, make the menu sound or mute.
# When installing in a mass storage device, remove the EFI applications
# previously installed by this script.

# Limitations: this script only handles EFI images accessible at time of
# running it, stored in an ESP with a FAT file system, with a partition
# table labeled either msdos or gpt.
# It can't distinguish with certainty EFI boot loaders from other EFI
# files, hence can display entries for these pother EFI files in the
# boot menu.

# Permanent files written:
# An EFI application in the form of PE32+ executable that gives access
# to EFI boot-loaders, stored in /boot as EFI3Mx64.efi and copied in its
# installation locations as BOOTx64.EFI, written by grub.mkimage.
# A configuration file grub.cfg stored alongside and read by BOOTx64.EFI
# in the target ESP, as its md5 checksum.
# /var/EFI3M stores one line per boot entry, with fields allowing to
# store customized labels and record hidden ones.
# /var/EFI3Minstall records the UUID of the ESP where the PE+32 image
# and associated grub.cfg are installed and the path to
# these files inside the ESP
# A NVRAM variable in the firmware linking to the PE32+ image written by
# this script in an ESP as /EFI/EFI3M/BOOTx64.EFI
# /etc/EFI3M/timeout stores the timeout in seconds before starting
# the first boot entry, if set by the user.
# /etc/EFI3M/sound stores the choice of a sound (y) or mute
# (n) boot menu, if set by the user or because at least one of the
# applications brltty, espeakup or orca was running when writing the
# configuration file

# Purpose of temporary files used:
# TMP: main temporary dir. All other temporary files are in $TMP
# MENU: menu as will be displayed when running the PE32+ executable
# NEWMENU: temporary MENU while reordered
# LINE: a line of $MENU being edited
# ESPLIST: list of EFI System Partitions accessible at time of running
# this script
# ACCESSIBLE: lists the EFI loaders accessible at time of running this
# script will be copied to /var/EFI3M
# MNT: directory used as mount point for the ESP listed in ESPLIST. We
# assume that if an ESP is already mounted, probably on /boot/efi, it is
# with read and write permissions for root. Also used as mount point for
# the USB stick on which write the PE32+ image and grub.cfg
# BEFORE, AFTER: register the lists of USB sticks resp. before and after
# the user should have plugged in the one on which to write the PE32+
# image grub.cfg that will be written in the target ESP
#
# Commands used by this script are shipped in:
# coreutils, efibootmgr, efivar, file, grep, gptfdisk, grub-2.02, parted,
# util-linux in a Slint or Slackware distribution. A run-time dependency
# to grub could be avoided shipping a pre-built PE32+ image. This will
# be proposed in a further release.

# Initialization
if [ $(id -u) -ne 0 ]; then
	echo "Only root may run this script."
	exit
fi
if [ ! -d /sys/firmware/efi ]; then
	echo "EFI booting is not enabled, game over."
	exit
fi
# The UEFI specification states that an EFI System partition has a GUID
# of C12A7328-F81F-11D2-BA4B-00A0C93EC93B for a GPT layout.
# In case of a DOS layout instead, an ESP should have an OS type of
# 0xEF. lsblk writes these values in the same field PARTTYPE.
ESPPARTTYPE=C12A7328-F81F-11D2-BA4B-00A0C93EC93B
OSTYPE=0xEF
# Ref: https://fr.wikipedia.org/wiki/Note_de_musique
SCALE="26163 29366 32963 34923 39200 44000 49388"
#      do    ré    mi    fa    sol   la    si
HALT="play 240 523 1 392 1 330 1 262 1"
REBOOT="play 240 523 1 392 1 330 1 262 1 330 1 392 1 523 1"
TMP=$(mktemp -d)
MNT=$TMP/MNT
mkdir -p $MNT
mkdir -p /etc/EFI3M
ESPLIST=$TMP/ESPLIST
DEFAULT_TIMEOUT=10
HELLO_SOUND="play 480 440 1"

# Main functions:
#
# install_on_system_ESP: makes and install the EFI boot manager on a
# mass storage device permanently attached to the computer.
#
# install_on_USB_stick: wipes out and format an USB stick with an ESP
# and installs the EFI boot manager there.
#
# install_in_firmware: insert an entry for the boot manager in the
# firmware's boot menu.
#
# edit_the_menu: allow the user to hide or show a menu entry, modify
# the label of a menu entry or change the order of the boot entries

features() {
clear
echo \
"The EFI Multi Boot Menu Maker (EFI3M) allows booting any installed
system for which an EFI boot loader is found on the computer. It comes
handy if you can't boot otherwise some installed system.

Features:
1) Build a boot menu and install it in an EFI system partition of your
   computer, in a specific location and, if not already busy, optionally
   in a fall back location where the firmware should look at priority.
2) Install the boot menu on an USB stick. Then you can boot off the USB
   stick, which in turn will present you with the boot menu allowing to
   boot any of the installed systems. This helps in case for some reason
   the internal boot menu be not displayed.
3) Customize the boot menu: hide a menu entry, edit its displayed label,
   modify the order in which the entries are displayed, set the delay
   before auto boot, mute or sound the menu.
   All modifications will be automatically applied to the internal boot
   menu; to apply them to the USB stick you will need to write again the
   menu on it.

Caveat: the menu built by EFI3M can include entries for EFI files that
are not boot loaders, or not working ones. Just hide them editing it.
"
printf "Press Enter to continue. "
read dummy
clear
clear
echo \
"Requirements:
GRUB version at least 2.02 should be installed, as well as usual
utilities found in pretty much any Linux distribution.

Sound aid for the visually impaired:
After booting, navigate in the menu with the up and down arrow keys.
If brltty, espeakup or orca is active when EFI3M is run:
1) A Beep is emitted as soon as the menu is displayed.
2) When pressing Enter on the menu entry n (numbered from the top of
   the menu), a tune of n sounds is played. The user can then confirm
   this choice pressing Enter (the same tune is then played again) or go
   back to the top of the menu pressing the Esc key.
3) The same apply to the the last entries, Halt and Reboot. A descending
   arpeggio is played for Halt, a descending followed by an ascending
   arpeggio for Reboot. Again, confirm with Enter or cancel with Esc.

The sound can also be set afterwards: type 4 in the main menu of EFI3M,
then M for Mute or S for Sound, and validate the setting typing V.

To apply the modification to an USB stick you will need to write again
the menu on it, typing 3 in the main menu.
"
printf "Press enter to go back to the main menu "
read dummy
}

to_lower() {
echo "$1"|tr '[:upper:]' '[:lower:]'
}

frequency() {
	# We won't play a tune for more than 35 entries...
	ENTRYNUM=$1
	if [ $ENTRYNUM -lt 8 ]; then
		OCTAVE=1
	elif [ $ENTRYNUM -lt 15 ]; then
		OCTAVE=2
	elif [ $ENTRYNUM -lt 22 ]; then
		OCTAVE=3
	elif [ $ENTRYNUM -lt 29 ]; then
		OCTAVE=4
	elif [ $ENTRYNUM -lt 36 ]; then
		OCTAVE=5
	else
		echo ""
		return
	fi
	NUMNOTE=$(($ENTRYNUM-(${OCTAVE}-1)*7))
	CENTIHERZ=$(($(echo $SCALE|cut -d" " -f$NUMNOTE)*${OCTAVE}))
	REMAIN=0
	if [ $(($CENTIHERZ % 100)) -gt 49 ]; then
		REMAIN=1
	fi
	FREQUENCY=$((($CENTIHERZ/100)+$REMAIN))
	echo $FREQUENCY
}

playtune() {
	TUNE="play 480 "
	i=1
	while [ $i -le $1 ]; do
		TUNE="$TUNE $(frequency $i) 2"
		i=$((${i}+1))
	done
	echo $TUNE
}

one_space() {
	echo "$1"|sed "s/ \{1,\}/ /g"
}

set_mountpoint() {
	ESP_NAME=$(one_space "$(lsblk -l -o uuid,name)"|grep $UUID|cut -d" " -f 2)
	MOUNTPOINT=$(one_space "$(df -h)"|grep /dev/$ESP_NAME|cut -d" " -f 6)
	ESP=/dev/$ESP_NAME
	MOUNTED=y
	if [ "$MOUNTPOINT" = "" ]; then
		MOUNTPOINT=$MNT
		mount $ESP $MNT
		MOUNTED=n
	fi
}

remove_previous_files() {
	if [ ! -f /var/EFI3Minstall ]; then
		return
	fi
	while read UUID EFIPATH; do
		set_mountpoint
		EFIDIR=$(dirname $EFIPATH)
		if [ ! -d $MOUNTPOINT$EFIDIR ]; then
			# Don't try to cd to a directory that have been removed
			# from ESP in this machine after a previous installation
			continue
		fi
		( cd $MOUNTPOINT$EFIDIR
		if [ -f EFI3M.md5 ]; then
			if md5sum -c --quiet EFI3M.md5; then
				rm BOOTx64.EFI
				rm EFI3M.md5
			fi
		fi
		if [ -f EFI3Mgrub.md5 ]; then
			if md5sum -c --quiet EFI3Mgrub.md5; then
				rm grub.cfg
				rm EFI3Mgrub.md5
			fi
		fi
		)
		if [ "$MOUNTED" = "n" ]; then
			umount $MNT
		fi
	done < /var/EFI3Minstall
}

display_the_menu() {
	clear
	if [ ! -f /var/EFI3M ]; then
		printf %b "The boot menu does not exist yet. We can generate it now, but then don't\n"
		printf %b "forget to install it afterwards in this computer or an USB stick!\n"
		printf "Do you want to generate the menu now [y/N]? "
		read dummy
		if [ ! "$(to_lower $dummy)" = "y" ]; then
			return
		else
			clear
			make_boot_menu
		fi
	fi
	printf %b "The boot menu will be displayed like this, if and when installed in the\n"
	printf %b "computer or an USB stick:\n\n"
	while read name uuid path label; do
		if [ "$label" = "LABEL=hidden" ]; then
			continue
		elif [ "$label" = "LABEL=defaultscheme" ]; then
			echo "($name)$path"
		else
			echo "${label#LABEL=}"
		fi
	done < /var/EFI3M
	echo "Shut down the computer"
	echo "Reboot the computer"
	printf  %b "\nPress Enter to continue. "
	read dummy
}

display_install_path() {
	if [ "$1" = "main" ]; then
		SHOW=$(grep EFI3M /var/EFI3Minstall)
	else
		SHOW=$(grep /EFI/BOOT /var/EFI3Minstall)
	fi
	UUID=$(echo $SHOW|cut -d" " -f1)
	ESP_PATH=$(echo $SHOW|cut -d" " -f2)
	PARTITION=$(one_space "$(lsblk -l -o UUID,NAME)"|grep $UUID|cut -d" " -f2)
	printf %b "/dev/$PARTITION as ${ESP_PATH}.\n"
}

update_grub() {
	if [ ! -f $ESPLIST ]; then
		list_the_ESP
	fi
	while read NAME PARTTYPE UUID PARENT; do
		set_mountpoint
		( cd $MOUNTPOINT
		MD5=$(find -name EFI3Mgrub.md5)
		if [ ! "$MD5" = "" ]; then
			for i in $MD5; do
				( cd ${i%/EFI3Mgrub.md5};
				if md5sum -c --quiet EFI3Mgrub.md5; then
					cp $TMP/grub.cfg grub.cfg
					md5sum grub.cfg > EFI3Mgrub.md5
				fi
				)
			done
		fi
		)
		if [ "$MOUNTED" = "n" ]; then
			umount $MNT
		fi
	done < $ESPLIST
}

build_boot_loader() {
	grub_mkimage="$(find /bin/ /usr/bin /sbin /usr/sbin  -type f -name grub-mkimage -perm -u+x)"
	grub2_mkimage="$(find /bin/ /usr/bin /sbin /usr/sbin  -type f -name grub2-mkimage -perm -u+x)"
	if [ "$grub_mkimage" = "" ]; then
		if  [ "$grub2_mkimage" = "" ]; then
			echo "Neither grub-mkimage nor grub2-mkimage have been found."
			echo "Maybe grub (I mean grub2) is not installed?"
			printf "Press Enter to quit."
			read dummy
			the_end
		else
			grub_mkimage=$grub2_mkimage
		fi
	fi
	if [ ! "$(echo "$grub_mkimage"|wc -l)" = "1" ]; then
		grub_mkimage=$(echo $grub_mkimage|sed -n 1p)
	fi
	GRUB_VERSION=$($(basename $grub_mkimage) -V | sed "s/.* //g")
	if ! echo $GRUB_VERSION | grep -q "2.0[2|3|4|6]"; then
		echo "GRUB version at least 2.02 is required but version $GRUB_VERSION is installed."
		printf "Press Enter to quit. "
		read dummy
		the_end
	fi
	$(basename $grub_mkimage) \
	--format=x86_64-efi \
	--output=/boot/EFI3Mx64.efi \
	--prefix="" \
	--compression=xz \
	part_gpt part_msdos fat play chain reboot halt search search_fs_uuid help at_keyboard usb_keyboard usb sleep
}
# part_gpt part_msdos fat play chain reboot halt search search_fs_uuid efi_gop efi_uga all_video loadbios help at_keyboard usb_keyboard usb sleep extcmd normal
store_default_settings() {
	if [ ! "$USERTIMEOUT" = "" ]; then
		echo $USERTIMEOUT > /etc/EFI3M/timeout
	fi
	if [ ! "$USERSOUND" = "" ]; then
		echo $USERSOUND > /etc/EFI3M/sound
	fi
}

write_config_file_with_sound() {
	echo "$HELLO_SOUND" >> $TMP/grub.cfg
	# Write a stanza for each line of /var/EFI3M
	while read NAME UUID EFIPATH LABEL; do
		if echo $LABEL|grep -q "^LABEL=hidden"; then
			continue
		elif echo $LABEL|grep -q "^LABEL=defaultscheme"; then
			LABEL="($NAME)$EFIPATH"
		else LABEL=$(echo $LABEL|sed "s^LABEL=")
		fi
		NUMENTRY=$((${NUMENTRY}+1))
		PLAYTUNE=""
		PLAYTUNE="$(playtune $NUMENTRY)"
		cat <<-EOF >> $TMP/grub.cfg
		submenu "$LABEL" {
			$PLAYTUNE
			menuentry "start now $LABEL" {
				insmod part_gpt
				insmod part_msdos
				insmod fat
				search --fs-uuid --set=root $UUID
				chainloader $EFIPATH
				$PLAYTUNE
			}
		}
		EOF
	done < /var/EFI3M
	# Add global features.
	cat <<-EOF >> $TMP/grub.cfg
	submenu "Shut down computer" {
		$HALT
		menuentry "Confirm shut down computer" {
			$HALT
			halt
		}
	}
	submenu "Reboot computer" {
		$REBOOT
		menuentry "Confirm Reboot computer" {
			$REBOOT
			reboot
		}
	}
	EOF
}
write_config_file_without_sound() {
	# Write a stanza for each line of /var/EFI3M
	while read NAME UUID EFIPATH LABEL; do
		if echo $LABEL|grep -q "^LABEL=hidden"; then
			continue
		elif echo $LABEL|grep -q "^LABEL=defaultscheme"; then
			LABEL="($NAME)$EFIPATH"
		else LABEL=$(echo $LABEL|sed "s^LABEL=")
		fi
		cat <<-EOF >> $TMP/grub.cfg
		menuentry "$LABEL" {
			insmod part_gpt
			insmod part_msdos
			insmod fat
			search --fs-uuid --set=root $UUID
			chainloader $EFIPATH
		}
		EOF
	done < /var/EFI3M
	# Add global features.
	cat <<-EOF >> $TMP/grub.cfg
	menuentry "Shut down computer" {
		halt
	}
	menuentry "Reboot computer" {
		reboot
	}
	EOF
}

write_grub_config_file() {
	if [ -f /etc/EFI3M/timeout ]; then
		TIMEOUT=$(grep . /etc/EFI3M/timeout)
		# Just in case /etc/EFI3M/timeout has an unexpected content
		if echo $TIMEOUT|grep -q "[^[:digit:]]"; then
			TIMEOUT=$DEFAULT_TIMEOUT
		fi
			if [ $TIMEOUT -lt 5 ] || [ $TIMEOUT -gt 60 ]; then
			TIMEOUT=$DEFAULT_TIMEOUT
		fi
	fi
	if [ "$TIMEOUT" = "" ]; then
		TIMEOUT=$DEFAULT_TIMEOUT
	fi
	if [ -f /etc/EFI3M/sound ]; then
		USERSOUND=$(grep . /etc/EFI3M/sound)
	fi
	# Just in case /etc/EFI3M/sound has an unexpected content
	[ ! "$USERSOUND" = "n" ] && [ ! "$USERSOUND" = "y" ] && USERSOUND=""
	if [ "$USERSOUND" = "" ]; then
		if [ ! "$(ps -C espeakup --noheaders)" = "" ] || \
		[ ! "$(ps -C orca --noheaders)" = "" ] || \
		[ ! "$(ps -C brltty --noheaders)" = "" ] || \
		[ "$USERSOUND" = "y" ]; then
			SOUND=y
		else
			SOUND=n
		fi
	else
		SOUND=$USERSOUND
	fi
	NUMENTRY=0
	# Write our specific default settings
	cat <<-EOF >$TMP/grub.cfg
	# Configuration file written by EFI3M, the EFI Multi-boot Manager Maker.
	set timeout=$TIMEOUT
	set menu_color_normal=white/black
	set menu_color_highlight=white/blue
	EOF
	if [ "$SOUND" = "y"  ]; then
		write_config_file_with_sound
	else
		write_config_file_without_sound
	fi
}

list_the_ESP() {
	# List the accessible EFI system partitions or ESPs
	lsblk -l -o name,parttype,uuid,pkname|\
	grep -i -F -e "$ESPPARTTYPE" -e "$OSTYPE"|sed "s/ \{1,\}/ /g" > $ESPLIST
	if [ ! -s $ESPLIST ]; then # No EFI partitions
		echo "No EFI partition found, game over."
		return
	fi

}

make_boot_menu() {
	# Write a line for each menu entry in the file $ACCESSIBLE that will
	# be moved to /var/EFI3M, creating or updating it.
	# We remove the stuff we previously installed in all accessible ESP
	# thus they won't be included in grub.cfg, which will be written
	# from /var/EFI3M.
	# For this reason this function will only be called by
	# install_on_system_ESP, implying that install_on_USB_stick will
	# call install_on_system_ESP, not directly this function. But then
	# we won't suggest at the end of install_on_system_ESP to also
	# install on firmware. This won't hurt as anyway installing on
	# firmware needs to have already written EFI file in an ESP of which
	# the path will be written in the firmware menu boot entry.
	list_the_ESP
	ALREADY=$TMP/ALREADY
	NEW=$TMP/NEW
	touch $ALREADY $NEW
	#
	# We successively mount each ESP to find the boot loaders, and
	# (re)build the menu accordingly.
	# We take into account the previous customization of the menu.
	while read NAME PARTTYPE UUID PARENT; do
		# We discard the ESP on USB sticks.
		TRANSPORT=$(one_space "$(lsblk -l -o name,tran)"|grep "^$PARENT "|cut -d" " -f 2)
		HOTPLUG=$(one_space "$(lsblk -l -o name,hotplug)"|grep "^$PARENT "|cut -d" " -f 2)
		if [ "$TRANSPORT" = "usb" ] && [ "$HOTPLUG" = "1" ]; then
			continue
		fi
		# We mount successively each ESP to find the EFI boot loaders
		# that they could contain.
		set_mountpoint
		( cd $MOUNTPOINT
		# EFIPATHS will list all paths to efi boot loaders in this ESP
		EFIPATHS=$(find -iname "*.efi"|sed s/.//)
		# Select the EFI boot loaders and write a line for each.
		# For now we don't exclude the GRUB and rEFInd boot managers
		# although it be somehow redundant. This could be made optional.
		# We include all files ending in .efi not written by this script
		# and fulfilling at least one of the following conditions:
		# "file" says they are 'PE32+ executable' and 'EFI application'
		# They lie in the "fallback" directory /EFI/BOOT
		# They lie in /EFI/tools.
		for EFIPATH in $EFIPATHS; do
			EFIAPP=$(file $MOUNTPOINT$EFIPATH|grep 'PE32+ executable'|grep 'EFI application')
			FALLBACK=$(echo $MOUNTPOINT$EFIPATH|grep -i /EFI/BOOT)
			TOOLS=$(echo $MOUNTPOINT$EFIPATH|grep -i /EFI/tools)
			# REFIND=$(echo $MOUNTPOINT$EFIPATH|grep -i /efi/refind/refind_x64.efi)
			INCLUDE="n"
			if [ ! "$EFIAPP" = "" ] || [ ! "$FALLBACK" = "" ] || [ ! "$TOOLS" = "" ]; then
				INCLUDE="y"
			fi
			if [ "$INCLUDE" = "n" ]; then
				continue
			fi
			# /var/EFI3M stores a menu with customized labels
			# in lines formatted this way:
			# Lines with a customized label:
			# $NAME $UUID $EFIPATH LABEL=<label>
			# Lines without a customized label:
			# $NAME $UUID $EFIPATH LABEL=defaultscheme
			# Hidden lines:
			# $NAME $UUID $EFIPATH LABEL=hidden
			#
			# When processing the currently accessible boot loaders we
			# match their UUID with the ones in /var/EFI3M,
			# to apply the recorded customization to the new menu,
			# append to or remove from /var/EFI3M lines
			# corresponding to efi boot loaders newly accessible or no
			# more accessible, respectively.
			LABEL="($NAME)$EFIPATH"
			if [ -f /var/EFI3M ]; then
				LINE=$(grep $EFIPATH /var/EFI3M|grep $UUID)
				if [ "$LINE" = "" ]; then
					# not yet registered in /var/EFI3M
					LINE="$NAME $UUID $EFIPATH LABEL=defaultscheme"
					echo "$LINE" >> $NEW
				else CHOICE="$(echo $LINE|sed "s/.*LABEL=//")"
					if [ "$CHOICE" = "hidden" ]; then
						echo "$LINE" >> $ALREADY
						continue
					elif [ ! "$CHOICE" = "defaultscheme" ];then
						LABEL=$CHOICE
						echo "$LINE" >> $ALREADY
					else
						echo "$LINE" >> $ALREADY
					fi
				fi
			else
				LINE="$NAME $UUID $EFIPATH LABEL=defaultscheme"
				echo "$LINE" >> $NEW
			fi
		done
		)
		if [ "$MOUNTED" = "n" ]; then
			umount $MNT
		fi
	done < $ESPLIST
	# update /var/EFI3M
	# We will first write in /var/EFI3M the lines previously and still
	# there then the new ones. This is to avoid changing the order of
	# the boot entries possibly set by the user.
	# We exclude entries previously installed by us.
	if [ -f /var/EFI3M ]; then
		mv /var/EFI3M /var/EFI3M.orig
		while read NAME UUID EFIPATH LABEL; do
			if [ ! -f /var/EFI3Minstall ]; then
				INSTALLED=""
			else
				INSTALLED=$(grep $UUID /var/EFI3Minstall|grep $EFIPATH)
			fi
			if [ "$INSTALLED" = "" ]; then
				echo "$NAME $UUID $EFIPATH $LABEL" >> /var/EFI3M
			fi
		done < /var/EFI3M.orig
	fi
	while read NAME UUID EFIPATH LABEL; do
		if [ ! -f /var/EFI3Minstall ]; then
			INSTALLED=""
		else
			INSTALLED=$(grep $UUID /var/EFI3Minstall|grep $EFIPATH)
		fi
		if [ "$INSTALLED" = "" ]; then
			echo "$NAME $UUID $EFIPATH $LABEL" >> /var/EFI3M
		fi
	done < $NEW
	rm $ALREADY $NEW
	if [ -f /var/EFI3M.orig ]; then
		rm /var/EFI3M.orig
	fi
}

install_on_system_ESP() {
	clear
	echo "Please wait..."
	list_the_ESP
	# Output one line per ESP with these fields:
	# name parttype uuid pkname
	# pkname is the name of the device (parent of the partition)
	remove_previous_files efibootmenu
	remove_previous_files EFI3M
	make_boot_menu
	write_grub_config_file
	build_boot_loader
	# Initially we choose as ESP in which to write the EFI boot manager
	# the first one listed by lsblk, but if there is one in the device
	# that hosts the / partition we will prefer it.
	ESP_DEVICE=/dev/$(head -n 1 $ESPLIST|cut -d" " -f4)
	UUID=$(head -n 1 $ESPLIST|cut -d" " -f3)
	ROOT_UUID=$(findmnt -n -o UUID --mountpoint /)
	# The root device is the parent of the root partition, listed by
	# lsblk as PKNAME
	ROOT_NAME=$(one_space "$(lsblk -l -o UUID,PKNAME)"|grep $ROOT_UUID|cut -d" " -f2)
	while read name parttype uuid parent; do
		if [ "$parent" = "$ROOT_NAME" ]; then
			ESP="/dev/$name"
			ESP_DEVICE=/dev/"$parent"
			UUID="$uuid"
			break
		fi
	done < $ESPLIST
	set_mountpoint
	# We record the md5 checksum of BOOTx64.EFI alongside it.
	# We will propose to install the boot menu as /EFI/BOOT/BOOTx64.EFI
	# in $ESP if it doesn't exist yet or if the existing one has been
	# written by this script, which we check against its md5 checksum.
	INSTALLDIR=$MOUNTPOINT/EFI/EFI3M
	if [ ! -d "$INSTALLDIR" ]; then
		mkdir -p $INSTALLDIR
	fi
	cp /boot/EFI3Mx64.efi $INSTALLDIR/BOOTx64.EFI
	cp $TMP/grub.cfg $INSTALLDIR
	( cd $INSTALLDIR
	md5sum BOOTx64.EFI > EFI3M.md5
	md5sum grub.cfg > EFI3Mgrub.md5
	)
	echo "$UUID /EFI/EFI3M/BOOTx64.EFI" > /var/EFI3Minstall
	clear
	printf "The boot menu has been installed in "
	display_install_path main
	DEFAULTDIR=$MOUNTPOINT/EFI/BOOT
	DEFAULT=n
	if [ ! -d $DEFAULTDIR ]; then
		DEFAULT=y
	elif [ ! -f $DEFAULTDIR/BOOTx64.EFI ] && \
	[ ! -f $DEFAULTDIR/grub.cfg ]; then
		DEFAULT=y
	# We removed the previous files installed by this script from this
	# system, but $DEFAULTDIR/BOOTx64 could have been written by this
	# script run from another system on the same machine. If that is the
	# case, we may replace it.
	else
		( cd $DEFAULTDIR
		if [ -f EFI3M.md5 ]; then
			if md5sum -c --quiet EFI3M.md5; then
				rm BOOTx64.EFI
				rm EFI3M.md5
			fi
		fi
		if [ -f EFI3Mgrub.md5 ]; then
			if md5sum -c --quiet EFI3Mgrub.md5; then
				rm grub.cfg
				rm EFI3Mgrub.md5
			fi
		fi
		)
		if [ ! -f $DEFAULTDIR/BOOTx64.EFI ] && \
		[ ! -f $DEFAULTDIR/grub.cfg ]; then
		DEFAULT=y
		fi
	fi
	if [ "$DEFAULT" = "y" ]; then
		mkdir -p $DEFAULTDIR
		CHOICE="n"
		echo "Do you want to also install it in the default BOOT directory?"
		printf %b "This can help insure that the boot menu will be displayed [y/N] "
		read CHOICE
		if [ "$(to_lower "$CHOICE")" = "y" ]; then
			cp $INSTALLDIR/BOOTx64.EFI $DEFAULTDIR
			cp $INSTALLDIR/grub.cfg $DEFAULTDIR
			cp $INSTALLDIR/EFI3M.md5 $DEFAULTDIR
			cp $INSTALLDIR/EFI3Mgrub.md5 $DEFAULTDIR
			echo "$UUID /EFI/BOOT/BOOTx64.EFI" >> /var/EFI3Minstall
			printf "The boot menu has also been installed in "
			display_install_path fallback
		else
			echo
		fi
	fi
	# Remove the boot entries to PE32+ executable that we previously
	# wrote, case occurring. There should be at most one.
	BOOTLIST=$(efibootmgr |grep -i "^BOOT"|efibootmgr |grep -i "^BOOT"|grep "EFI Boot menu"|sed "s/....\(....\).*/\1/")
	for i in $BOOTLIST; do
		efibootmgr -q -B -b $i
	done
	if [ "$MOUNTED" = "n" ]; then
		umount $MNT
	fi
	printf "Do you want to add an entry for the boot menu in the firmware? [y/N] "
	read FIRMWARE
	if [ "$(to_lower $FIRMWARE)" = "y" ]; then
		install_in_firmware
	fi
}

install_in_firmware() {
	# We are optionally called by install_on_system_ESP.
	PARTNUM=$(echo $ESP|sed "s,${ESP_DEVICE},,")
	PARTNUM=$(echo $PARTNUM|sed "s/[^[:digit:]]*//g"	)
	printf %b "\nThis entry has been added to the firmware's boot menu:\n"
	REPORT=$(efibootmgr -v -c -d $ESP_DEVICE -p $PARTNUM -l "\\EFI\\EFI3M\\BOOTx64.EFI" -L "EFI Boot menu")
	echo "$REPORT"|grep "EFI Boot menu"
	printf "Press Enter to continue. "
	read dummy
}

install_on_USB_stick() {
	if [ ! -f /var/EFI3M ]; then
		make_boot_menu
	fi
	if [ ! -f $TMP/grub.cfg ]; then
		write_grub_config_file
	fi
	build_boot_loader
	BADSTICK="n"
	STICK="y"
	BEFORE=$TMP/BEFORE
	AFTER=$TMP/AFTER
	while true; do
		clear
		if [ "$BADSTICK" = "y" ]; then
			printf "Do you want to try again making an USB boot stick? [y/N]? "
			read STICK
		fi
		if [ ! "$(to_lower $STICK)" = "y" ]; then
			rm $BEFORE $AFTER
			return
		fi
		clear
		lsblk -l -o name,type,tran,hotplug|grep disk|grep usb|grep 1$|sort|sed "s/  .*//" > $BEFORE
		printf %b "WARNING: the content of the USB stick will be irrecoverably deleted!\n\n"
		printf "Type Q to quit, or plug in the USB stick. Then, press Enter: "
		read ANSWER
		sleep 1
		if [ "$(to_lower "$ANSWER")" = "q" ]; then
			rm $BEFORE
			return
		fi
		lsblk -l -o name,type,tran,hotplug|grep disk|grep usb|grep 1$|sort|sed "s/  .*//" > $AFTER
		STICKNAME=""
		while read NEW_DEVICE; do
			if [ "$(grep $NEW_DEVICE $BEFORE)" = "" ]; then
				STICKNAME=$NEW_DEVICE
			fi
		done < $AFTER
		if [ "$STICKNAME" = "" ]; then
			printf %b "\nNo new device detected.\n"
			printf %b "If the USB stick was already inserted, please remove it now.\n"
			printf "Press Enter to continue. "
			read dummy
			BADSTICK="y"
			continue
		else
			clear
			VENDOR=$(lsblk -l -o vendor,name|grep "${STICKNAME}$"|sed "s/[^[:space:]]*$//")
			MODEL=$(lsblk -l -o model,name|grep "${STICKNAME}$"|sed "s/[^[:space:]]*$//")
			SIZE=$(lsblk -l -o size,name|grep "${STICKNAME}$"|sed "s/[^[:space:]]*$//")
			printf %b "Found a device with these characteristics\n":
			printf %b  "Vendor: $VENDOR\n"
			printf %b "Model:  $MODEL\n"
			printf %b "Size:   $SIZE\n"
			printf %b "\nPlease check carefully this information, not to wipe the wrong device!\n\n"
			printf "Is it the good device? [y/N]: "
			read GOOD
			if [ "$(to_lower $GOOD)" = "y" ]; then
				# If a partition of the USB stick is already mounted,
				# bail out. This can occur in case of auto-mounting of
				# removable devices.
				if [ ! "$(one_space "$(df -h)"|grep /dev/$STICKNAME|cut -d" " -f 6)" = "" ]; then
					printf %b "\nA partition of this USB stick is already mounted.\n"
					printf %b "Please unmount it or choose another device, then try again.\n"
					printf "Press Enter to continue. "
					read dummy
					BADSTICK="y"
					continue
				fi
				break
			else
				clear
				printf "Please remove this device, then press Enter: "
				BADSTICK="y"
				read dummy
				continue
			fi
		fi

	done
	STICKDEVICE=/dev/$STICKNAME
	### Format the USB stick and copy the EFI image there
	dd if=/dev/zero status=none of=$STICKDEVICE bs=512 count=1
	sgdisk -Z $STICKDEVICE 1>/dev/null
	sgdisk -n 1:2048:4095 -c:1:"EFI System Partition" -t 1:ef00 $STICKDEVICE 1>/dev/null
	partprobe $STICKDEVICE
	STICKESP=${STICKDEVICE}1
	mkfs.msdos -c -F12 $STICKESP 1>/dev/null
	mount $STICKESP $MNT
	mkdir -p $MNT/EFI/BOOT
	cp $TMP/grub.cfg $MNT/EFI/BOOT
	cp /boot/EFI3Mx64.efi $MNT/EFI/BOOT/BOOTx64.EFI
	umount $MNT
	rm $BEFORE $AFTER
	echo "The boot menu has been written on the USB stick."
	printf "Press Enter to continue. "
	read dummy
}

edit_label() {
LINE=$TMP/LINE
while true; do
	grep ^$CHOICE $MENU>$LINE
	read LINENUMBER name uuid path label < $LINE
	clear
	echo "This boot entry:"
	echo "$path in partition /dev/$name"
	if [ "$label" = "LABEL=hidden" ]; then
		echo "will not be displayed."
	elif [ "$label" = "LABEL=defaultscheme" ]; then
		echo "will be displayed by default:"
		echo "($name)$path"
	else
		echo "will be displayed: \"${label#LABEL=}\""
		echo
	fi
	printf %b "\nPlease type either:\n"
	printf %b "H to Hide this boot entry,\n"
	printf %b "D to use the Default Label\n"
	printf %b "or the new label (5 to 60 characters, spaces allowed but not quotes)."
	printf %b "\n\nJust press Enter to accept the current setting.\n"
	printf %b "\nPlease type the new label or the letter D or H now: "
	read LABEL
	[ "$LABEL" = "" ] && rm $LINE && return
	if [ "$(to_lower "$LABEL")" = "h" ]; then
		sed "/^$LINENUMBER /s.*$LINENUMBER $name $uuid $path LABEL=hidden" $MENU > somedummmyfile
		mv somedummmyfile $MENU
		rm $LINE
		continue
	elif   [ "$(to_lower "$LABEL")" = "d" ]; then
		sed "/^$LINENUMBER /s.*$LINENUMBER $name $uuid $path LABEL=defaultscheme" $MENU > somedummmyfile
		mv somedummmyfile $MENU
		rm $LINE
		continue
	else
		if [ ${#LABEL} -lt 5 ] || [ ${#LABEL} -gt 60 ]; then
			printf %b "The label should have between 5 and 4O characters.\n"
			printf "Press Enter then try again. "
			read dummy
			continue
		elif echo $LABEL|grep -q -e '"' -e "'"; then
			printf %b "Characters \" and ' are not allowed in the label.\n"
			printf "Press Enter then try again. "
			read dummy
			continue
		elif echo $LABEL|grep -q "[^[:print:]]"; then
			printf %b "Only printable characters, including spaces, are allowed in the label\n".
			printf "Press Enter then try again. "
			read dummy
			continue
		else
			sed "/^$LINENUMBER /s.*$LINENUMBER $name $uuid $path LABEL=$LABEL" $MENU > somedummmyfile
			mv somedummmyfile $MENU
			rm $LINE
			continue
		fi
		rm $LINE
		return
	fi
done
}

move_up() {

		if [ $NUMBER -gt 2 ]; then
			sed -n 1,$((${NUMBER}-2))p $MENU>>$NEWMENU
		fi
		sed -n ${NUMBER}p $MENU>>$NEWMENU
		sed -n $((${NUMBER}-1))p $MENU>>$NEWMENU
		sed -n $((${NUMBER}+1)),${MAXLINENUMBER}p $MENU>>$NEWMENU
		NEWNUMBER=0
		printf "" > $MENU
		while read  OLDNUMBER name uuid path label; do
			NEWNUMBER=$(($NEWNUMBER + 1))
			echo "$NEWNUMBER $name $uuid $path $label" >> $MENU
		done < $NEWMENU
		printf "" > $NEWMENU
		NUMBER=$(( $NUMBER - 1 ))
		unset -v C1 C2
		MOVE=""
}
move_down() {
		if [ $NUMBER -gt 1 ]; then
			sed -n 1,$((${NUMBER}-1))p $MENU>>$NEWMENU
		fi
		sed -n $((${NUMBER}+1))p $MENU>>$NEWMENU
		sed -n ${NUMBER}p $MENU>>$NEWMENU
		if [ $NUMBER -lt $((${MAXLINENUMBER}-1)) ]; then
			sed -n $((${NUMBER}+2)),${MAXLINENUMBER}p $MENU>>$NEWMENU
		fi
		NEWNUMBER=0
		printf "" > $MENU
		while read OLDNUMBER name uuid path label; do
			NEWNUMBER=$(($NEWNUMBER + 1))
			echo "$NEWNUMBER $name $uuid $path $label" >> $MENU
		done < $NEWMENU
		printf "" > $NEWMENU
		NUMBER=$(($NUMBER + 1))
		unset -v C1 C2
		MOVE=""
}

change_the_boot_order() {
	MOVE=""
	NEWMENU=$TMP/NEWMENU
	MENU="$1"
	MAXLINENUMBER=$2
	while true; do
		clear
		echo "Current menu:"
		while read LINENUMBER name uuid path label; do
			printf $LINENUMBER
			# Only the 99 first lines will be properly aligned ;)
			if [ ${#LINENUMBER} -eq 1 ]; then
				printf " "
			fi
			if [ "$label" = "LABEL=hidden" ]; then
				echo " ($name)$path (not displayed)"
			elif [ "$label" = "LABEL=defaultscheme" ]; then
				echo " ($name)$path"
			else
				echo " ${label#LABEL=}"
			fi
		done < $MENU
		printf %b "\nType the number of the line to move, press Enter when you are done: "
		read NUMBER
		if [ "$NUMBER" = "" ]; then
			rm $NEWMENU
			return
		fi
		if echo $NUMBER|grep -q "[^[:digit:]]"; then
			printf "Not a number, press Enter to continue."
			read dummy
			continue
		fi
		if [ $NUMBER -lt 1 ] || [ $NUMBER -gt $MAXLINENUMBER ]; then
			echo "The line number should be between 1 and $MAXLINENUMBER."
			printf "Press Enter to continue. "
			read dummy
			continue
		fi
		clear
		while true; do
			echo "Current menu:"
			while read LINENUMBER name uuid path label; do
				if [ $LINENUMBER -eq $NUMBER ]; then
					printf "*"
				else
					printf " "
				fi
				if [ "$label" = "LABEL=hidden" ]; then
					echo " ($name)$path (not displayed)"
				elif [ "$label" = "LABEL=defaultscheme" ]; then
					echo " ($name)$path"
				else
					echo " ${label#LABEL=}"
				fi
			done < $MENU
			printf %b "\nUse the arrow keys to move the selected boot entry up or down,"
			printf %b "\npress enter when you are done "
			while true; do
				previous_tty_settings=$(stty -g)
				stty -icanon -echo
				# KEY=$(head -c1)
				KEY=$(dd bs=1 count=1 2>/dev/null)
				stty "$previous_tty_settings"
				case $KEY in
					"") break 2 ;;
					) C1="ESC" ;;
					[) [ "$C1" = "ESC" ] && C2="[" ;;
					A) [ "$C2" = "[" ] && MOVE=up ;;
					B) [ "$C2" = "[" ] && MOVE=down ;;
					*) ;;
				esac
				if [ "$MOVE" = "down" ] && [ $NUMBER -lt $MAXLINENUMBER ]; then
					clear
					move_down
					continue 2
				fi
				if [ "$MOVE" = "up" ] && [ $NUMBER -gt 1 ]; then
					clear
					move_up
					continue 2
				fi
			done
		done
	done
}

edit_the_menu() {
	make_boot_menu
	CHOICE=""
	MENU=$TMP/MENU
	LINENUMBER=0
	while read line; do
		LINENUMBER=$(($LINENUMBER + 1))
		echo "$LINENUMBER $line" >> $MENU
	done < /var/EFI3M
	MAXLINENUMBER=$LINENUMBER
	while true; do
		clear
		echo "Current menu (custom labels on the second line):"
		echo
		while read LINENUMBER name uuid path label; do
			printf $LINENUMBER
			# Only the 99 first lines will be properly aligned ;)
			if [ ${#LINENUMBER} -eq 1 ]; then
				printf " "
			fi
			if [ "$label" = "LABEL=hidden" ]; then
				echo " ($name)$path (not displayed)"
			elif [ "$label" = "LABEL=defaultscheme" ]; then
				echo " ($name)$path"
			else
				echo " ($name)$path"
				echo "   Label: ${label#LABEL=}"
			fi

		done < $MENU
		printf %b "\nType either:"
		printf %b "\nthe number of a boot entry to customize its label,"
		printf %b "\nD to set the Delay before auto boot,"
		printf %b "\nM for a Mute menu, S for a menu with Sound,"
		if [ $MAXLINENUMBER -gt 1 ]; then
			printf %b  "\nO to modify the boot Order,"
		fi
		printf %b "\nV to Validate your modifications."
		printf "\nJust press Enter to undo all modifications and return to the main menu."
		printf %b "\n\nType the number or letter that corresponds to your choice. "
		read CHOICE
		if [ "$CHOICE" = "" ]; then
			rm $MENU
			return
		fi
		if [ "$(to_lower "$CHOICE")" = "d" ]; then
			while true; do
				clear
				if [ -f /etc/EFI3M/timeout ]; then
					TIMEOUT=$(grep . /etc/EFI3M/timeout)
					if echo $TIMEOUT|grep -q "[^[:digit:]]"; then
						TIMEOUT=$DEFAULT_TIMEOUT
					fi
					if [ $TIMEOUT -lt 5 ] || [ $TIMEOUT -gt 60 ]; then
						TIMEOUT=$DEFAULT_TIMEOUT
					fi
				else
					TIMEOUT=$DEFAULT_TIMEOUT
				fi
				printf %b "The current time out before automatically booting the first menu entry is: \n"
				printf %b "$TIMEOUT seconds\n\n"
				printf %b "Please type the new time out or just press Enter to accept the current one. "
				read USERTIMEOUT
				if [ "$USERTIMEOUT" = "" ]; then
					USERTIMEOUT=$TIMEOUT
					break
				fi
				if echo $USERTIMEOUT|grep -q "[^[:digit:]]"; then
					printf "Not a number, press Enter and try again. "
					read dummy
					continue
				fi
				if [ $USERTIMEOUT -lt 5 ] || [ $USERTIMEOUT -gt 60 ]; then
					printf "The time out should be between 5 and 60, press Enter and try again. "
					read dummy
					continue
				fi
				echo "The time out will be $USERTIMEOUT seconds if you validate this setting."
				printf %b "Press Enter to continue. "
				read dummy
				break
			done
		elif [ "$(to_lower "$CHOICE")" = "s" ]; then
			USERSOUND=y
			clear
			echo "You will have a menu with sound if you validate this setting."
			printf "Press Enter to continue. "
			read dummy
		elif [ "$(to_lower "$CHOICE")" = "m" ]; then
			USERSOUND=n
			clear
			echo "You will have a mute menu if you validate this setting."
			printf "Press Enter to continue. "
			read dummy
		elif [ "$(to_lower "$CHOICE")" = "v" ]; then
			sed "s^[^ ]*" $MENU > /var/EFI3M
			store_default_settings
			write_grub_config_file
			update_grub
			rm $MENU
			clear
			printf %b "The boot menu has been updated. If not already done, you can install it\n"
			printf %b "in your computer. You will need to write it again on an USB stick to\n"
			printf %b "update it there, case occurring.\n\n"
			printf "Press Enter to continue. "
			read dummy
			return
		elif [ "$(to_lower "$CHOICE")" = "o" ] && [ $MAXLINENUMBER -ge 2 ]; then
			change_the_boot_order $MENU $MAXLINENUMBER
		elif echo $CHOICE|grep -q "[^[:digit:]]"; then
			printf  "Wrong typing, press Enter to continue."
			read dummy
		elif [ $CHOICE -eq 0 ] || [ $CHOICE -gt $MAXLINENUMBER ]; then
			printf %b "The line number should be between 1 and $MAXLINENUMBER.\n"
			printf "Press Enter to continue. "
			read dummy
		else
			edit_label
		fi
	done
}

the_end() {
	rm -f $ESPLIST
	rm -f $TMP/grub.cfg
	rmdir $MNT
	rmdir $TMP
	clear
	echo "Good bye."
	exit
}

while true; do
	clear
	printf %b "EFI multiboot menu maker (EFI3M)\n\n"
	printf %b "Available features:\n"
	printf %b "1 Display features and usage\n"
	printf %b "2 Install the multiboot menu on your computer\n"
	printf %b "3 Make a rescue USB stick providing the boot menu\n"
	printf %b "4 Customize the boot menu\n"
	printf %b "5 Display the boot menu\n\n"
	printf %b "Just press Enter to quit.\n\n"
	printf "Type the number in front of the chosen feature: "
	read CHOICE
	case $CHOICE in
		1) features ;;
		2) install_on_system_ESP ;;
		3) install_on_USB_stick ;;
		4) edit_the_menu ;;
		5) display_the_menu ;;
		*) the_end
	esac
done
