\ Copyright (c) 2003 Scott Long <scottl@FreeBSD.org>
\ Copyright (c) 2003 Aleksander Fafula <alex@fafula.com>
\ Copyright (c) 2006-2015 Devin Teske <dteske@FreeBSD.org>
\ All rights reserved.
\ 
\ Redistribution and use in source and binary forms, with or without
\ modification, are permitted provided that the following conditions
\ are met:
\ 1. Redistributions of source code must retain the above copyright
\    notice, this list of conditions and the following disclaimer.
\ 2. Redistributions in binary form must reproduce the above copyright
\    notice, this list of conditions and the following disclaimer in the
\    documentation and/or other materials provided with the distribution.
\ 
\ THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``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 OR CONTRIBUTORS 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.
\ 
\ $FreeBSD: stable/12/stand/forth/menu.4th 370542 2021-09-12 05:43:58Z git2svn $

marker task-menu.4th

\ Frame drawing
include /boot/frames.4th

vocabulary menu-infrastructure
vocabulary menu-namespace
vocabulary menu-command-helpers

only forth also menu-infrastructure definitions

f_double        \ Set frames to double (see frames.4th). Replace with
                \ f_single if you want single frames.
46 constant dot \ ASCII definition of a period (in decimal)

 5 constant menu_default_x         \ default column position of timeout
10 constant menu_default_y         \ default row position of timeout msg
 4 constant menu_timeout_default_x \ default column position of timeout
23 constant menu_timeout_default_y \ default row position of timeout msg
10 constant menu_timeout_default   \ default timeout (in seconds)

\ Customize the following values with care

  1 constant menu_start \ Numerical prefix of first menu item
dot constant bullet     \ Menu bullet (appears after numerical prefix)
  5 constant menu_x     \ Row position of the menu (from the top)
 10 constant menu_y     \ Column position of the menu (from left side)

\ Menu Appearance
variable menuidx   \ Menu item stack for number prefixes
variable menurow   \ Menu item stack for positioning
variable menubllt  \ Menu item bullet

\ Menu Positioning
variable menuX     \ Menu X offset (columns)
variable menuY     \ Menu Y offset (rows)

\ Menu-item elements
variable menurebootadded

\ Parsing of kernels into menu-items
variable kernidx
variable kernlen
variable kernmenuidx

\ Menu timer [count-down] variables
variable menu_timeout_enabled \ timeout state (internal use only)
variable menu_time            \ variable for tracking the passage of time
variable menu_timeout         \ determined configurable delay duration
variable menu_timeout_x       \ column position of timeout message
variable menu_timeout_y       \ row position of timeout message

\ Containers for parsing kernels into menu-items
create kerncapbuf 64 allot
create kerndefault 64 allot
create kernelsbuf 256 allot

only forth also menu-namespace definitions

\ Menu-item key association/detection
variable menukey1
variable menukey2
variable menukey3
variable menukey4
variable menukey5
variable menukey6
variable menukey7
variable menukey8
variable menureboot
variable menuacpi
variable menuoptions
variable menukernel

\ Menu initialization status variables
variable init_state1
variable init_state2
variable init_state3
variable init_state4
variable init_state5
variable init_state6
variable init_state7
variable init_state8

\ Boolean option status variables
variable toggle_state1
variable toggle_state2
variable toggle_state3
variable toggle_state4
variable toggle_state5
variable toggle_state6
variable toggle_state7
variable toggle_state8

\ Array option status variables
variable cycle_state1
variable cycle_state2
variable cycle_state3
variable cycle_state4
variable cycle_state5
variable cycle_state6
variable cycle_state7
variable cycle_state8

\ Containers for storing the initial caption text
create init_text1 64 allot
create init_text2 64 allot
create init_text3 64 allot
create init_text4 64 allot
create init_text5 64 allot
create init_text6 64 allot
create init_text7 64 allot
create init_text8 64 allot

only forth definitions

: arch-i386? ( -- BOOL ) \ Returns TRUE (-1) on i386, FALSE (0) otherwise.
	s" arch-i386" environment? dup if
		drop
	then
;

: acpipresent? ( -- flag ) \ Returns TRUE if ACPI is present, FALSE otherwise
	s" hint.acpi.0.rsdp" getenv
	dup -1 = if
		drop false exit
	then
	2drop
	true
;

: acpienabled? ( -- flag ) \ Returns TRUE if ACPI is enabled, FALSE otherwise
	s" hint.acpi.0.disabled" getenv
	dup -1 <> if
		s" 0" compare 0<> if
			false exit
		then
	else
		drop
	then
	true
;

: +c! ( N C-ADDR/U K -- C-ADDR/U )
	3 pick 3 pick	( n c-addr/u k -- n c-addr/u k n c-addr )
	rot + c!	( n c-addr/u k n c-addr -- n c-addr/u )
	rot drop	( n c-addr/u -- c-addr/u )
;

only forth also menu-namespace definitions

\ Forth variables
: namespace     ( C-ADDR/U N -- ) also menu-namespace +c! evaluate previous ;
: menukeyN      ( N -- ADDR )   s" menukeyN"       7 namespace ;
: init_stateN   ( N -- ADDR )   s" init_stateN"   10 namespace ;
: toggle_stateN ( N -- ADDR )   s" toggle_stateN" 12 namespace ;
: cycle_stateN  ( N -- ADDR )   s" cycle_stateN"  11 namespace ;
: init_textN    ( N -- C-ADDR ) s" init_textN"     9 namespace ;

\ Environment variables
: kernel[x]          ( N -- C-ADDR/U )   s" kernel[x]"           7 +c! ;
: menu_init[x]       ( N -- C-ADDR/U )   s" menu_init[x]"       10 +c! ;
: menu_command[x]    ( N -- C-ADDR/U )   s" menu_command[x]"    13 +c! ;
: menu_caption[x]    ( N -- C-ADDR/U )   s" menu_caption[x]"    13 +c! ;
: ansi_caption[x]    ( N -- C-ADDR/U )   s" ansi_caption[x]"    13 +c! ;
: menu_keycode[x]    ( N -- C-ADDR/U )   s" menu_keycode[x]"    13 +c! ;
: toggled_text[x]    ( N -- C-ADDR/U )   s" toggled_text[x]"    13 +c! ;
: toggled_ansi[x]    ( N -- C-ADDR/U )   s" toggled_ansi[x]"    13 +c! ;
: menu_caption[x][y] ( N M -- C-ADDR/U ) s" menu_caption[x][y]" 16 +c! 13 +c! ;
: ansi_caption[x][y] ( N M -- C-ADDR/U ) s" ansi_caption[x][y]" 16 +c! 13 +c! ;

also menu-infrastructure definitions

\ This function prints a menu item at menuX (row) and menuY (column), returns
\ the incremental decimal ASCII value associated with the menu item, and
\ increments the cursor position to the next row for the creation of the next
\ menu item. This function is called by the menu-create function. You need not
\ call it directly.
\ 
: printmenuitem ( menu_item_str -- ascii_keycode )

	loader_color? if [char] ^ escc! then

	menurow dup @ 1+ swap ! ( increment menurow )
	menuidx dup @ 1+ swap ! ( increment menuidx )

	\ Calculate the menuitem row position
	menurow @ menuY @ +

	\ Position the cursor at the menuitem position
	dup menuX @ swap at-xy

	\ Print the value of menuidx
	loader_color? dup ( -- bool bool )
	if b then
	menuidx @ .
	if me then

	\ Move the cursor forward 1 column
	dup menuX @ 1+ swap at-xy

	menubllt @ emit	\ Print the menu bullet using the emit function

	\ Move the cursor to the 3rd column from the current position
	\ to allow for a space between the numerical prefix and the
	\ text caption
	menuX @ 3 + swap at-xy

	\ Print the menu caption (we expect a string to be on the stack
	\ prior to invoking this function)
	type

	\ Here we will add the ASCII decimal of the numerical prefix
	\ to the stack (decimal ASCII for `1' is 49) as a "return value"
	menuidx @ 48 +
;

\ This function prints the appropriate menuitem basename to the stack if an
\ ACPI option is to be presented to the user, otherwise returns -1. Used
\ internally by menu-create, you need not (nor should you) call this directly.
\ 
: acpimenuitem ( -- C-Addr/U | -1 )

	arch-i386? if
		acpipresent? if
			acpienabled? if
				loader_color? if
					s" toggled_ansi[x]"
				else
					s" toggled_text[x]"
				then
			else
				loader_color? if
					s" ansi_caption[x]"
				else
					s" menu_caption[x]"
				then
			then
		else
			menuidx dup @ 1+ swap ! ( increment menuidx )
			-1
		then
	else
		-1
	then
;

: delim? ( C -- BOOL )
	dup  32 =		( c -- c bool )		\ [sp] space
	over  9 = or		( c bool -- c bool )	\ [ht] horizontal tab
	over 10 = or		( c bool -- c bool )	\ [nl] newline
	over 13 = or		( c bool -- c bool )	\ [cr] carriage return
	over [char] , =	or	( c bool -- c bool )	\ comma
	swap drop		( c bool -- bool )	\ return boolean
;

\ This function parses $kernels into variables that are used by the menu to
\ display which kernel to boot when the [overloaded] `boot' word is interpreted.
\ Used internally by menu-create, you need not (nor should you) call this
\ directly.
\ 
: parse-kernels ( N -- ) \ kernidx
	kernidx ! ( n -- )	\ store provided `x' value
	[char] 0 kernmenuidx !	\ initialize `y' value for menu_caption[x][y]

	\ Attempt to get a list of kernels, fall back to sensible default
	s" kernels" getenv dup -1 = if
		drop ( cruft )
		s" kernel kernel.old"
	then ( -- c-addr/u )

	\ Check to see if the user has altered $kernel by comparing it against
	\ $kernel[N] where N is kernel_state (the actively displayed kernel).
	s" kernel_state" evaluate @ 48 + s" kernel[N]" 7 +c! getenv
	dup -1 <> if
		s" kernel" getenv dup -1 = if
			drop ( cruft ) s" "
		then
		2swap 2over compare 0= if
			2drop FALSE ( skip below conditional )
		else \ User has changed $kernel
			TRUE ( slurp in new value )
		then
	else \ We haven't yet parsed $kernels into $kernel[N]
		drop ( getenv cruft )
		s" kernel" getenv dup -1 = if
			drop ( cruft ) s" "
		then
		TRUE ( slurp in initial value )
	then ( c-addr/u -- c-addr/u c-addr/u,-1 | 0 )
	if \ slurp new value into kerndefault
		kerndefault 1+ 0 2swap strcat swap 1- c!
	then

	\ Clear out existing parsed-kernels
	kernidx @ [char] 0
	begin
		dup kernel[x] unsetenv
		2dup menu_caption[x][y] unsetenv
		2dup ansi_caption[x][y] unsetenv
		1+ dup [char] 8 >
	until
	2drop

	\ Step through the string until we find the end
	begin
		0 kernlen ! \ initialize length of value

		\ Skip leading whitespace and/or comma delimiters
		begin
			dup 0<> if
				over c@ delim? ( c-addr/u -- c-addr/u bool )
			else
				false ( c-addr/u -- c-addr/u bool )
			then
		while
			1- swap 1+ swap ( c-addr/u -- c-addr'/u' )
		repeat
		( c-addr/u -- c-addr'/u' )

		dup 0= if \ end of string while eating whitespace
			2drop ( c-addr/u -- )
			kernmenuidx @ [char] 0 <> if \ found at least one
				exit \ all done
			then

			\ No entries in $kernels; use $kernel instead
			s" kernel" getenv dup -1 = if
				drop ( cruft ) s" "
			then ( -- c-addr/u )
			dup kernlen ! \ store entire value length as kernlen
		else
			\ We're still within $kernels parsing toward the end;
			\ find delimiter/end to determine kernlen
			2dup ( c-addr/u -- c-addr/u c-addr/u )
			begin dup 0<> while
				over c@ delim? if
					drop 0 ( break ) \ found delimiter
				else
					kernlen @ 1+ kernlen ! \ incrememnt
					1- swap 1+ swap \ c-addr++ u--
				then
			repeat
			2drop ( c-addr/u c-addr'/u' -- c-addr/u )

			\ If this is the first entry, compare it to $kernel
			\ If different, then insert $kernel beforehand
			kernmenuidx @ [char] 0 = if
				over kernlen @ kerndefault count compare if
					kernelsbuf 0 kerndefault count strcat
					s" ," strcat 2swap strcat
					kerndefault count swap drop kernlen !
				then
			then
		then
		( c-addr/u -- c-addr'/u' )

		\ At this point, we should have something on the stack to store
		\ as the next kernel menu option; start assembling variables

		over kernlen @ ( c-addr/u -- c-addr/u c-addr/u2 )

		\ Assign first to kernel[x]
		2dup kernmenuidx @ kernel[x] setenv

		\ Assign second to menu_caption[x][y]
		kerncapbuf 0 s" [K]ernel: " strcat
		2over strcat
		kernidx @ kernmenuidx @ menu_caption[x][y]
		setenv

		\ Assign third to ansi_caption[x][y]
		kerncapbuf 0 s" @[1mK@[mernel: " [char] @ escc! strcat
		kernmenuidx @ [char] 0 = if
			s" default/@[32m"
		else
			s" @[34;1m"
		then
		[char] @ escc! strcat
		2over strcat
		s" @[m" [char] @ escc! strcat
		kernidx @ kernmenuidx @ ansi_caption[x][y]
		setenv

		2drop ( c-addr/u c-addr/u2 -- c-addr/u )

		kernmenuidx @ 1+ dup kernmenuidx ! [char] 8 > if
			2drop ( c-addr/u -- ) exit
		then

		kernlen @ - swap kernlen @ + swap ( c-addr/u -- c-addr'/u' )
	again
;

\ This function goes through the kernels that were discovered by the
\ parse-kernels function [above], adding " (# of #)" text to the end of each
\ caption.
\ 
: tag-kernels ( -- )
	kernidx @ ( -- x ) dup 0= if exit then
	[char] 0 s"  (Y of Z)" ( x -- x y c-addr/u )
	kernmenuidx @ -rot 7 +c! \ Replace 'Z' with number of kernels parsed
	begin
		2 pick 1+ -rot 2 +c! \ Replace 'Y' with current ASCII num

		2over menu_caption[x][y] getenv dup -1 <> if
			2dup + 1- c@ [char] ) = if
				2drop \ Already tagged
			else
				kerncapbuf 0 2swap strcat
				2over strcat
				5 pick 5 pick menu_caption[x][y] setenv
			then
		else
			drop ( getenv cruft )
		then

		2over ansi_caption[x][y] getenv dup -1 <> if
			2dup + 1- c@ [char] ) = if
				2drop \ Already tagged
			else
				kerncapbuf 0 2swap strcat
				2over strcat
				5 pick 5 pick ansi_caption[x][y] setenv
			then
		else
			drop ( getenv cruft )
		then

		rot 1+ dup [char] 8 > if
			-rot 2drop TRUE ( break )
		else
			-rot FALSE
		then
	until
	2drop ( x y -- )
;

\ This function creates the list of menu items. This function is called by the
\ menu-display function. You need not call it directly.
\ 
: menu-create ( -- )

	\ Print the frame caption at (x,y)
	s" loader_menu_title" getenv dup -1 = if
		drop s" Welcome to FreeBSD"
	then
	TRUE ( use default alignment )
	s" loader_menu_title_align" getenv dup -1 <> if
		2dup s" left" compare-insensitive 0= if ( 1 )
			2drop ( c-addr/u ) drop ( bool )
			menuX @ menuY @ 1-
			FALSE ( don't use default alignment )
		else ( 1 ) 2dup s" right" compare-insensitive 0= if ( 2 )
			2drop ( c-addr/u ) drop ( bool )
			menuX @ 42 + 4 - over - menuY @ 1-
			FALSE ( don't use default alignment )
		else ( 2 ) 2drop ( c-addr/u ) then ( 1 ) then
	else
		drop ( getenv cruft )
	then
	if ( use default center alignement? )
		menuX @ 19 + over 2 / - menuY @ 1-
	then
	swap 1- swap
	at-xy dup 0= if
		2drop ( empty loader_menu_title )
	else
		space type space
	then

	\ If $menu_init is set, evaluate it (allowing for whole menus to be
	\ constructed dynamically -- as this function could conceivably set
	\ the remaining environment variables to construct the menu entirely).
	\ 
	s" menu_init" getenv dup -1 <> if
		evaluate
	else
		drop
	then

	\ Print our menu options with respective key/variable associations.
	\ `printmenuitem' ends by adding the decimal ASCII value for the
	\ numerical prefix to the stack. We store the value left on the stack
	\ to the key binding variable for later testing against a character
	\ captured by the `getkey' function.

	\ Note that any menu item beyond 9 will have a numerical prefix on the
	\ screen consisting of the first digit (ie. 1 for the tenth menu item)
	\ and the key required to activate that menu item will be the decimal
	\ ASCII of 48 plus the menu item (ie. 58 for the tenth item, aka. `:')
	\ which is misleading and not desirable.
	\ 
	\ Thus, we do not allow more than 8 configurable items on the menu
	\ (with "Reboot" as the optional ninth and highest numbered item).

	\ 
	\ Initialize the ACPI option status.
	\ 
	0 menuacpi !
	s" menu_acpi" getenv -1 <> if
		c@ dup 48 > over 57 < and if ( '1' <= c1 <= '8' )
			menuacpi !
			arch-i386? if acpipresent? if
				\ 
				\ Set menu toggle state to active state
				\ (required by generic toggle_menuitem)
				\ 
				acpienabled? menuacpi @ toggle_stateN !
			then then
		else
			drop
		then
	then

	\ 
	\ Initialize kernel captions after parsing $kernels
	\ 
	0 menukernel !
	s" menu_kernel" getenv -1 <> if
		c@ dup 48 > over 57 < and if ( '1' <= c1 <= '8' )
			dup menukernel !
			dup parse-kernels tag-kernels

			\ Get the current cycle state (entry to use)
			s" kernel_state" evaluate @ 48 + ( n -- n y )

			\ If state is invalid, reset
			dup kernmenuidx @ 1- > if
				drop [char] 0 ( n y -- n 48 )
				0 s" kernel_state" evaluate !
				over s" init_kernel" evaluate drop
			then

			\ Set the current non-ANSI caption
			2dup swap dup ( n y -- n y y n n )
			s" set menu_caption[x]=$menu_caption[x][y]"
			17 +c! 34 +c! 37 +c! evaluate
			( n y y n n c-addr/u -- n y  )

			\ Set the current ANSI caption
			2dup swap dup ( n y -- n y y n n )
			s" set ansi_caption[x]=$ansi_caption[x][y]"
			17 +c! 34 +c! 37 +c! evaluate
			( n y y n n c-addr/u -- n y )

			\ Initialize cycle state from stored value
			48 - ( n y -- n k )
			s" init_cyclestate" evaluate ( n k -- n )

			\ Set $kernel to $kernel[y]
			s" activate_kernel" evaluate ( n -- n )
		then
		drop
	then

	\ 
	\ Initialize the menu_options visual separator.
	\ 
	0 menuoptions !
	s" menu_options" getenv -1 <> if
		c@ dup 48 > over 57 < and if ( '1' <= c1 <= '8' )
			menuoptions !
		else
			drop
		then
	then

	\ Initialize "Reboot" menu state variable (prevents double-entry)
	false menurebootadded !

	menu_start
	1- menuidx !    \ Initialize the starting index for the menu
	0 menurow !     \ Initialize the starting position for the menu

	49 \ Iterator start (loop range 49 to 56; ASCII '1' to '8')
	begin
		\ If the "Options:" separator, print it.
		dup menuoptions @ = if
			\ Optionally add a reboot option to the menu
			s" menu_reboot" getenv -1 <> if
				drop
				s" Reboot" printmenuitem menureboot !
				true menurebootadded !
			then

			menuX @
			menurow @ 2 + menurow !
			menurow @ menuY @ +
			at-xy
			s" menu_optionstext" getenv dup -1 <> if
				type
			else
				drop ." Options:"
			then
		then

		\ If this is the ACPI menu option, act accordingly.
		dup menuacpi @ = if
			dup acpimenuitem ( n -- n n c-addr/u | n n -1 )
			dup -1 <> if
				13 +c! ( n n c-addr/u -- n c-addr/u )
				       \ replace 'x' with n
			else
				swap drop ( n n -1 -- n -1 )
				over menu_command[x] unsetenv
			then
		else
			\ make sure we have not already initialized this item
			dup init_stateN dup @ 0= if
				1 swap !

				\ If this menuitem has an initializer, run it
				dup menu_init[x]
				getenv dup -1 <> if
					evaluate
				else
					drop
				then
			else
				drop
			then

			dup
			loader_color? if
				ansi_caption[x]
			else
				menu_caption[x]
			then
		then

		dup -1 <> if
			\ test for environment variable
			getenv dup -1 <> if
				printmenuitem ( c-addr/u -- n )
				dup menukeyN !
			else
				drop
			then
		else
			drop
		then

		1+ dup 56 > \ add 1 to iterator, continue if less than 57
	until
	drop \ iterator

	\ Optionally add a reboot option to the menu
	menurebootadded @ true <> if
		s" menu_reboot" getenv -1 <> if
			drop       \ no need for the value
			s" Reboot" \ menu caption (required by printmenuitem)

			printmenuitem
			menureboot !
		else
			0 menureboot !
		then
	then
;

\ Takes a single integer on the stack and updates the timeout display. The
\ integer must be between 0 and 9 (we will only update a single digit in the
\ source message).
\ 
: menu-timeout-update ( N -- )

	\ Enforce minimum/maximum
	dup 9 > if drop 9 then
	dup 0 < if drop 0 then

	s" Autoboot in N seconds. [Space] to pause" ( n -- n c-addr/u )

	2 pick 0> if
		rot 48 + -rot ( n c-addr/u -- n' c-addr/u ) \ convert to ASCII
		12 +c!        ( n' c-addr/u -- c-addr/u )   \ replace 'N' above

		menu_timeout_x @ menu_timeout_y @ at-xy \ position cursor
		type ( c-addr/u -- ) \ print message
	else
		menu_timeout_x @ menu_timeout_y @ at-xy \ position cursor
		spaces ( n c-addr/u -- n c-addr ) \ erase message
		2drop ( n c-addr -- )
	then

	0 25 at-xy ( position cursor back at bottom-left )
;

\ This function blocks program flow (loops forever) until a key is pressed.
\ The key that was pressed is added to the top of the stack in the form of its
\ decimal ASCII representation. This function is called by the menu-display
\ function. You need not call it directly.
\ 
: getkey ( -- ascii_keycode )

	begin \ loop forever

		menu_timeout_enabled @ 1 = if
			( -- )
			seconds ( get current time: -- N )
			dup menu_time @ <> if ( has time elapsed?: N N N -- N )

				\ At least 1 second has elapsed since last loop
				\ so we will decrement our "timeout" (really a
				\ counter, insuring that we do not proceed too
				\ fast) and update our timeout display.

				menu_time ! ( update time record: N -- )
				menu_timeout @ ( "time" remaining: -- N )
				dup 0> if ( greater than 0?: N N 0 -- N )
					1- ( decrement counter: N -- N )
					dup menu_timeout !
						( re-assign: N N Addr -- N )
				then
				( -- N )

				dup 0= swap 0< or if ( N <= 0?: N N -- )
					\ halt the timer
					0 menu_timeout ! ( 0 Addr -- )
					0 menu_timeout_enabled ! ( 0 Addr -- )
				then

				\ update the timer display ( N -- )
				menu_timeout @ menu-timeout-update

				menu_timeout @ 0= if
					\ We've reached the end of the timeout
					\ (user did not cancel by pressing ANY
					\ key)

					s" menu_timeout_command"  getenv dup
					-1 = if
						drop \ clean-up
					else
						evaluate
					then
				then

			else ( -- N )
				\ No [detectable] time has elapsed (in seconds)
				drop ( N -- )
			then
			( -- )
		then

		key? if \ Was a key pressed? (see loader(8))

			\ An actual key was pressed (if the timeout is running,
			\ kill it regardless of which key was pressed)
			menu_timeout @ 0<> if
				0 menu_timeout !
				0 menu_timeout_enabled !

				\ clear screen of timeout message
				0 menu-timeout-update
			then

			\ get the key that was pressed and exit (if we
			\ get a non-zero ASCII code)
			key dup 0<> if
				exit
			else
				drop
			then
		then
		50 ms \ sleep for 50 milliseconds (see loader(8))

	again
;

: menu-erase ( -- ) \ Erases menu and resets positioning variable to position 1.

	\ Clear the screen area associated with the interactive menu
	menuX @ menuY @
	2dup at-xy 38 spaces 1+		2dup at-xy 38 spaces 1+
	2dup at-xy 38 spaces 1+		2dup at-xy 38 spaces 1+
	2dup at-xy 38 spaces 1+		2dup at-xy 38 spaces 1+
	2dup at-xy 38 spaces 1+		2dup at-xy 38 spaces 1+
	2dup at-xy 38 spaces 1+		2dup at-xy 38 spaces 1+
	2dup at-xy 38 spaces 1+		2dup at-xy 38 spaces
	2drop

	\ Reset the starting index and position for the menu
	menu_start 1- menuidx !
	0 menurow !
;

only forth
also menu-infrastructure
also menu-namespace
also menu-command-helpers definitions

: toggle_menuitem ( N -- N ) \ toggles caption text and internal menuitem state

	\ ASCII numeral equal to user-selected menu item must be on the stack.
	\ We do not modify the stack, so the ASCII numeral is left on top.

	dup init_textN c@ 0= if
		\ NOTE: no need to check toggle_stateN since the first time we
		\ are called, we will populate init_textN. Further, we don't
		\ need to test whether menu_caption[x] (ansi_caption[x] when
		\ loader_color?=1) is available since we would not have been
		\ called if the caption was NULL.

		\ base name of environment variable
		dup ( n -- n n ) \ key pressed
		loader_color? if
			ansi_caption[x]
		else
			menu_caption[x]
		then	
		getenv dup -1 <> if

			2 pick ( n c-addr/u -- n c-addr/u n )
			init_textN ( n c-addr/u n -- n c-addr/u c-addr )

			\ now we have the buffer c-addr on top
			\ ( followed by c-addr/u of current caption )

			\ Copy the current caption into our buffer
			2dup c! -rot \ store strlen at first byte
			begin
				rot 1+    \ bring alt addr to top and increment
				-rot -rot \ bring buffer addr to top
				2dup c@ swap c! \ copy current character
				1+     \ increment buffer addr
				rot 1- \ bring buffer len to top and decrement
				dup 0= \ exit loop if buffer len is zero
			until
			2drop \ buffer len/addr
			drop  \ alt addr

		else
			drop
		then
	then

	\ Now we are certain to have init_textN populated with the initial
	\ value of menu_caption[x] (ansi_caption[x] with loader_color enabled).
	\ We can now use init_textN as the untoggled caption and
	\ toggled_text[x] (toggled_ansi[x] with loader_color enabled) as the
	\ toggled caption and store the appropriate value into menu_caption[x]
	\ (again, ansi_caption[x] with loader_color enabled). Last, we'll
	\ negate the toggled state so that we reverse the flow on subsequent
	\ calls.

	dup toggle_stateN @ 0= if
		\ state is OFF, toggle to ON

		dup ( n -- n n ) \ key pressed
		loader_color? if
			toggled_ansi[x]
		else
			toggled_text[x]
		then
		getenv dup -1 <> if
			\ Assign toggled text to menu caption
			2 pick ( n c-addr/u -- n c-addr/u n ) \ key pressed
			loader_color? if
				ansi_caption[x]
			else
				menu_caption[x]
			then
			setenv
		else
			\ No toggled text, keep the same caption
			drop ( n -1 -- n ) \ getenv cruft
		then

		true \ new value of toggle state var (to be stored later)
	else
		\ state is ON, toggle to OFF

		dup init_textN count ( n -- n c-addr/u )

		\ Assign init_textN text to menu caption
		2 pick ( n c-addr/u -- n c-addr/u n ) \ key pressed
		loader_color? if
			ansi_caption[x]
		else
			menu_caption[x]
		then
		setenv

		false \ new value of toggle state var (to be stored below)
	then

	\ now we'll store the new toggle state (on top of stack)
	over toggle_stateN !
;

: cycle_menuitem ( N -- N ) \ cycles through array of choices for a menuitem

	\ ASCII numeral equal to user-selected menu item must be on the stack.
	\ We do not modify the stack, so the ASCII numeral is left on top.

	dup cycle_stateN dup @ 1+ \ get value and increment

	\ Before assigning the (incremented) value back to the pointer,
	\ let's test for the existence of this particular array element.
	\ If the element exists, we'll store index value and move on.
	\ Otherwise, we'll loop around to zero and store that.

	dup 48 + ( n addr k -- n addr k k' )
	         \ duplicate array index and convert to ASCII numeral

	3 pick swap ( n addr k k' -- n addr k n k' ) \ (n,k') as (x,y)
	loader_color? if
		ansi_caption[x][y]
	else
		menu_caption[x][y]
	then
	( n addr k n k' -- n addr k c-addr/u )

	\ Now test for the existence of our incremented array index in the
	\ form of $menu_caption[x][y] ($ansi_caption[x][y] with loader_color
	\ enabled) as set in loader.rc(5), et. al.

	getenv dup -1 = if
		\ No caption set for this array index. Loop back to zero.

		drop ( n addr k -1 -- n addr k ) \ getenv cruft
		drop 0 ( n addr k -- n addr 0 )  \ new value to store later

		2 pick [char] 0 ( n addr 0 -- n addr 0 n 48 ) \ (n,48) as (x,y)
		loader_color? if
			ansi_caption[x][y]
		else
			menu_caption[x][y]
		then
		( n addr 0 n 48 -- n addr 0 c-addr/u )
		getenv dup -1 = if
			\ Highly unlikely to occur, but to ensure things move
			\ along smoothly, allocate a temporary NULL string
			drop ( cruft ) s" "
		then
	then

	\ At this point, we should have the following on the stack (in order,
	\ from bottom to top):
	\ 
	\    n        - Ascii numeral representing the menu choice (inherited)
	\    addr     - address of our internal cycle_stateN variable
	\    k        - zero-based number we intend to store to the above
	\    c-addr/u - string value we intend to store to menu_caption[x]
	\               (or ansi_caption[x] with loader_color enabled)
	\ 
	\ Let's perform what we need to with the above.

	\ Assign array value text to menu caption
	4 pick ( n addr k c-addr/u -- n addr k c-addr/u n )
	loader_color? if
		ansi_caption[x]
	else
		menu_caption[x]
	then
	setenv

	swap ! ( n addr k -- n ) \ update array state variable
;

only forth definitions also menu-infrastructure

\ Erase and redraw the menu. Useful if you change a caption and want to
\ update the menu to reflect the new value.
\ 
: menu-redraw ( -- )
	menu-erase
	menu-create
;

\ This function initializes the menu. Call this from your `loader.rc' file
\ before calling any other menu-related functions.
\ 
: menu-init ( -- )
	menu_start
	1- menuidx !    \ Initialize the starting index for the menu
	0 menurow !     \ Initialize the starting position for the menu

	\ Assign configuration values
	s" loader_menu_y" getenv dup -1 = if
		drop \ no custom row position
		menu_default_y
	else
		\ make sure custom position is a number
		?number 0= if
			menu_default_y \ or use default
		then
	then
	menuY !
	s" loader_menu_x" getenv dup -1 = if
		drop \ no custom column position
		menu_default_x
	else
		\ make sure custom position is a number
		?number 0= if
			menu_default_x \ or use default
		then
	then
	menuX !

	\ Interpret a custom frame type for the menu
	TRUE ( draw a box? default yes, but might be altered below )
	s" loader_menu_frame" getenv dup -1 = if ( 1 )
		drop \ no custom frame type
	else ( 1 )  2dup s" single" compare-insensitive 0= if ( 2 )
		f_single ( see frames.4th )
	else ( 2 )  2dup s" double" compare-insensitive 0= if ( 3 )
		f_double ( see frames.4th )
	else ( 3 ) s" none" compare-insensitive 0= if ( 4 )
		drop FALSE \ don't draw a box
	( 4 ) then ( 3 ) then ( 2 )  then ( 1 ) then
	if
		42 13 menuX @ 3 - menuY @ 1- box \ Draw frame (w,h,x,y)
	then

	0 25 at-xy \ Move cursor to the bottom for output
;

also menu-namespace

\ Main function. Call this from your `loader.rc' file.
\ 
: menu-display ( -- )

	0 menu_timeout_enabled ! \ start with automatic timeout disabled

	\ check indication that automatic execution after delay is requested
	s" menu_timeout_command" getenv -1 <> if ( Addr C -1 -- | Addr )
		drop ( just testing existence right now: Addr -- )

		\ initialize state variables
		seconds menu_time ! ( store the time we started )
		1 menu_timeout_enabled ! ( enable automatic timeout )

		\ read custom time-duration (if set)
		s" autoboot_delay" getenv dup -1 = if
			drop \ no custom duration (remove dup'd bunk -1)
			menu_timeout_default \ use default setting
		else
			2dup ?number 0= if ( if not a number )
				\ disable timeout if "NO", else use default
				s" NO" compare-insensitive 0= if
					0 menu_timeout_enabled !
					0 ( assigned to menu_timeout below )
				else
					menu_timeout_default
				then
			else
				-rot 2drop

				\ boot immediately if less than zero
				dup 0< if
					drop
					menu-create
					0 25 at-xy
					0 boot
				then
			then
		then
		menu_timeout ! ( store value on stack from above )

		menu_timeout_enabled @ 1 = if
			\ read custom column position (if set)
			s" loader_menu_timeout_x" getenv dup -1 = if
				drop \ no custom column position
				menu_timeout_default_x \ use default setting
			else
				\ make sure custom position is a number
				?number 0= if
					menu_timeout_default_x \ or use default
				then
			then
			menu_timeout_x ! ( store value on stack from above )
        
			\ read custom row position (if set)
			s" loader_menu_timeout_y" getenv dup -1 = if
				drop \ no custom row position
				menu_timeout_default_y \ use default setting
			else
				\ make sure custom position is a number
				?number 0= if
					menu_timeout_default_y \ or use default
				then
			then
			menu_timeout_y ! ( store value on stack from above )
		then
	then

	menu-create

	begin \ Loop forever

		0 25 at-xy \ Move cursor to the bottom for output
		getkey     \ Block here, waiting for a key to be pressed

		dup -1 = if
			drop exit \ Caught abort (abnormal return)
		then

		\ Boot if the user pressed Enter/Ctrl-M (13) or
		\ Ctrl-Enter/Ctrl-J (10)
		dup over 13 = swap 10 = or if
			drop ( no longer needed )
			s" boot" evaluate
			exit ( pedantic; never reached )
		then

		dup menureboot @ = if 0 reboot then

		\ Evaluate the decimal ASCII value against known menu item
		\ key associations and act accordingly

		49 \ Iterator start (loop range 49 to 56; ASCII '1' to '8')
		begin
			dup menukeyN @
			rot tuck = if

				\ Adjust for missing ACPI menuitem on non-i386
				arch-i386? true <> menuacpi @ 0<> and if
					menuacpi @ over 2dup < -rot = or
					over 58 < and if
					( key >= menuacpi && key < 58: N -- N )
						1+
					then
				then

				\ Test for the environment variable
				dup menu_command[x]
				getenv dup -1 <> if
					\ Execute the stored procedure
					evaluate

					\ We expect there to be a non-zero
					\  value left on the stack after
					\ executing the stored procedure.
					\ If so, continue to run, else exit.

					0= if
						drop \ key pressed
						drop \ loop iterator
						exit
					else
						swap \ need iterator on top
					then
				then

				\ Re-adjust for missing ACPI menuitem
				arch-i386? true <> menuacpi @ 0<> and if
					swap
					menuacpi @ 1+ over 2dup < -rot = or
					over 59 < and if
						1-
					then
					swap
				then
			else
				swap \ need iterator on top
			then

			\ 
			\ Check for menu keycode shortcut(s)
			\ 
			dup menu_keycode[x]
			getenv dup -1 = if
				drop
			else
				?number 0<> if
					rot tuck = if
						swap
						dup menu_command[x]
						getenv dup -1 <> if
							evaluate
							0= if
								2drop
								exit
							then
						else
							drop
						then
					else
						swap
					then
				then
			then

			1+ dup 56 > \ increment iterator
			            \ continue if less than 57
		until
		drop \ loop iterator
		drop \ key pressed

	again	\ Non-operational key was pressed; repeat
;

\ This function unsets all the possible environment variables associated with
\ creating the interactive menu.
\ 
: menu-unset ( -- )

	49 \ Iterator start (loop range 49 to 56; ASCII '1' to '8')
	begin
		dup menu_init[x]    unsetenv	\ menu initializer
		dup menu_command[x] unsetenv	\ menu command
		dup menu_caption[x] unsetenv	\ menu caption
		dup ansi_caption[x] unsetenv	\ ANSI caption
		dup menu_keycode[x] unsetenv	\ menu keycode
		dup toggled_text[x] unsetenv	\ toggle_menuitem caption
		dup toggled_ansi[x] unsetenv	\ toggle_menuitem ANSI caption

		48 \ Iterator start (inner range 48 to 57; ASCII '0' to '9')
		begin
			\ cycle_menuitem caption and ANSI caption
			2dup menu_caption[x][y] unsetenv
			2dup ansi_caption[x][y] unsetenv
			1+ dup 57 >
		until
		drop \ inner iterator

		0 over menukeyN      !	\ used by menu-create, menu-display
		0 over init_stateN   !	\ used by menu-create
		0 over toggle_stateN !	\ used by toggle_menuitem
		0 over init_textN   c!	\ used by toggle_menuitem
		0 over cycle_stateN  !	\ used by cycle_menuitem

		1+ dup 56 >	\ increment, continue if less than 57
	until
	drop \ iterator

	s" menu_timeout_command" unsetenv	\ menu timeout command
	s" menu_reboot"          unsetenv	\ Reboot menu option flag
	s" menu_acpi"            unsetenv	\ ACPI menu option flag
	s" menu_kernel"          unsetenv	\ Kernel menu option flag
	s" menu_options"         unsetenv	\ Options separator flag
	s" menu_optionstext"     unsetenv	\ separator display text
	s" menu_init"            unsetenv	\ menu initializer

	0 menureboot !
	0 menuacpi !
	0 menuoptions !
;

only forth definitions also menu-infrastructure

\ This function both unsets menu variables and visually erases the menu area
\ in-preparation for another menu.
\ 
: menu-clear ( -- )
	menu-unset
	menu-erase
;

bullet menubllt !

also menu-namespace

\ Initialize our menu initialization state variables
0 init_state1 !
0 init_state2 !
0 init_state3 !
0 init_state4 !
0 init_state5 !
0 init_state6 !
0 init_state7 !
0 init_state8 !

\ Initialize our boolean state variables
0 toggle_state1 !
0 toggle_state2 !
0 toggle_state3 !
0 toggle_state4 !
0 toggle_state5 !
0 toggle_state6 !
0 toggle_state7 !
0 toggle_state8 !

\ Initialize our array state variables
0 cycle_state1 !
0 cycle_state2 !
0 cycle_state3 !
0 cycle_state4 !
0 cycle_state5 !
0 cycle_state6 !
0 cycle_state7 !
0 cycle_state8 !

\ Initialize string containers
0 init_text1 c!
0 init_text2 c!
0 init_text3 c!
0 init_text4 c!
0 init_text5 c!
0 init_text6 c!
0 init_text7 c!
0 init_text8 c!

only forth definitions
