$INQUIRE - prompt user for input using arrays +$CHOICES +$QKEYS

Press TAB for options or F1 for help:

@ > [:terminal/

.. -- go up to root
cuu -- move cursor up
el -- erase line
inkey -- read key
style -- set output text style

Small menu example with active help and (audio) feedback on error:

{
:local readKeyString do={
# written by msatter 2020-2021
# keyFlag show if the input was caused by a key pressing, firstDisplay is showing if the input was pre-provided by $4 
# Default characters allowed to use.
:set $errorName "characters"; #default error name.
:set $sound "\a"; # Warning sound. Put a "#" in front to silence it.
:set $upKey  false; # set $upKey to false to not indicate default a manual change of a earlier entry. 
:set $endKey false; # set $endKey to false
 :set $ASCI " !\" \$ &'()*+,-./0123456789:;<=>\?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}"; # allowed characters to be typed.
 :set $control [:pick $2 ([:find $2 ":"]) [:len $2]]; :set $2 [:pick $2 0 ([:find $2 ":"]+1)]; # Fill $control with the provided values in $2
 :if [:find $control *] do={:set $hide true;:set $unHide false} else={:set $hide false}
 :if [:find $control #] do={:set $allowedASCI "0123456789";:set $controlASCI "#";:set $errorName "numbers"};	# numbers allowed.
 :if [:find $control @] do={:set $allowedASCI "+-.0123456789@ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz";:set $controlASCI "@";:set $endCheck true;:set $endCheckRegex "^[a-z0-9.+_-]+\\@[a-z]+\\.[a-z]{2,7}\$"}; # e-mail address.
 :if [:find $control /] do={:set $allowedASCI "!\$&()*+,-./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz";:set $controlASCI "/";:set $endCheck true;:set $endCheckRegex "[\\.|^][a-z0-9._-]+\\.[a-z]{2,7}[/]{0,1}[a-zA-Z0-9\$_.+!*(),-]*"}; # web address.
 :if [:find $control I] do={:set $allowedASCI "/.0123456789";:set $controlASCI "I";:set $endCheck true;:set $errorName "IPv4 address";
 :set $endCheckRegex "^(25[0-5]|2[0-4][0-9]|[01][0-9][0-9]|[0-9][0-9]|[[0-9])\\.(25[0-5]|2[0-4][0-9]|[01][0-9][0-9]|[0-9][0-9]|[[0-9])\\.(25[0-5]|2[0-4][0-9]|[01][0-9][0-9]|[0-9][0-9]|[[0-9])\\.(25[0-5]|2[0-4][0-9]|[01][0-9][0-9]|[0-9][0-9]|[[0-9])(/(3[0-2]|2[0-9]|[1][0-9]|[0-9]))\?\$"}; # numbers and special signs allowed.
     
 :if [:find $control 6] do={:set $allowedASCI " :/0123456789";:set $controlASCI "#";:set $errorName "IPv6 address"}; # numbers and special signs allowed.
 
 # (3[0-2]|2[0-9]|[1][0-9]|[0-9]) CIDR Or :put [:typeof (255.168.21.7/24 in 0.0.0.0/0)] bool is ok and if nothing the the IP address was invalid.
  
 :set $minLength [:pick $3 0 [:find $3 "-"]]
 :set $maxLength [:pick $3 ([:find $3 "-"]+1) [:len $3]]
 :if  $hide   do={:set $1 "$1 TAB key shows entry."}; # Add to help text that the hidden content can be made visible
 # Adds the minimal an maximal length displayed in the Help line. If no length is defined then nothing is displayed.  
 :if ($minLength = $maxLength && $minLength != 0) do={:set $1 "$1 (Set length: $maxLength )"} else={
   :if ($minLength=0 && $maxLength>0 && $maxLength!=255)	do={:set $1 "$1 (Maximal length: $maxLength)"}
   :if ($minLength>0 && $maxLength>0 && $maxLength!=255)	do={:set $1 "$1 (Minimal length: $minLength, maximal length: $maxLength)"}
   :if ($minLength>0 && $maxLength=255) 			do={:set $1 "$1 (Minimal length: $minLength)"} }   
 :te st varname-local; :te el; :put "> Help:\t $1"; :te cuu; :te cuu; :te st syntax-noterm; # Displays the (adapted) help text
 :local i 0xFFFF; :set $keyFlag false; [:te el];:set $firstDisplay true;
 :put "$2 _"; # write first time the label with a cursor (_)
 
 :if ([:len $4] > $maxLength) do={[:te cuu;:te st varname]; :put "$2 $display \t Ignored the provided entry. Enter one yourself.$sound"; [:te st none];:set $4;:set $keyFlag false}
 :if ([:len $4] < $minLength && [:len $4] > 0) do={[:te cuu;:te st varname]; :put "$2 $4 \t Error: the pre-provided entry is to short. Add to, or replace this entry.$sound"; [:te st none]; :set $display $4;:set $readString $4;:set $4}
 
 :do {
  
  :while ( (!$endKey) ) do={
    :if ([:len $readString] > 0) do={    
      :if ($i=8) do={:set $keyFlag false; :set $readString "$[:pick $readString 0 ([:len $readString]-1)]"; :set $display "$[:pick $display 0 ([:len $readString])]"}      
      :if (($i=9) && $hide && (!$unHide))	do={:set $unHide true; :set $tempDisplay $display;:set $keyFlag false;:set $i}
      :if (($i=9) && $unHide && $hide)		do={:set $unHide false;:set $display [:pick "$tempDisplay******************" 0 [:len $readString]];:set $keyFlag false;:set $i} }            
    :if $keyFlag 			do={:set $temp "$[:pick $ASCI ($i-32) ($i-31)]";}; :set $i;
   
    :if [:find $control $controlASCI]	do={:if ([:find $allowedASCI $temp] >= 0) do={} else={:set $temp} }
    
    :if (([:typeof $temp]~"(nil|nothing)" && $keyFlag) && (!$firstDisplay)) do={[:te cuu;:te el;:te st varname];:put "$2 $display \t ERROR: ignored invalid key input.$sound"; [:te st none];:set $keyFlag false}
    
    :if (!firstDisplay) do={       
    :if (([:len $readString] >= $maxLength) && $keyFlag) do={[:te cuu;:te st varname]; :put "$2 $display \t Error: maximal $maxLength $errorName allowed.$sound";	[:te st none]; :set $temp; :set $keyFlag false} }    
    :if $keyFlag do={:set $readString "$readString$temp"};	                                        # Here the accepted character is added to $readString as long the length is less than maximum 
    :if ($hide) do={:if $keyFlag do={:set $display "$display*"};} else={:set $display $readString};	# Sets the displayed text 
    :if ($unHide) do={:set $display $readString};	                                                # Unhide text that is hidden when pressing the TAB key
    :if $firstDisplay do={:set $display $4;:set $readString $5;:set $firstDisplay false;:set $4;:set $5};	# Sets display to the provided string if it is the first view
   
 # Loop that displays and wait for a key to be pressed         
   :do {[:te cuu]; :put "$2 $display_ "; :set $i [:te in 500ms]; :if ($i=0xFFFF) do={[:te cuu;]; :put "$2 $display  "; :set $i [:te in 500ms]}} while=($i=0xFFFF); :set $keyFlag true; 
   [:te cuu;:te el]; :put "$2 $display  "; # erases any error messages shown
   :if ($i=60932) do={:set $i 13}; # The arrow down key is the same here as the Enter key and any changes will be checked this way.
   :if ($i=13 || $i=60931) do={:set $endKey true}; # This will enable to use one value to indicate the completion of the entry or go for an other entry up to for editing.
  }; #while=( (!$endKey)....
  :if ($i != 60931) do={ # Skip checks because these values where already checked before.
   :if ($unHide) do={[:te cuu;:te el]; :put "$2 $tempDisplay"; :set $display $tempDisplay}; # On exit hide the text again, which was made visible by using the TAB key.   
   :if (([:len $readString]>0)&&([:len $readString]<$minLength)) do={[:te cuu;:te st varname]; :put "$2 $display \t Error: not enough $errorName entered. Minimal length is: $minLength.$sound";[:te st none]; :set $endkey false;:set $keyFlag false}
   :if (([:len $readString] = 0) && ($minLength > 0)) do={[:te cuu;:te st varname]; :put "$2 $display \t Error: this entry can't be left empty.$sound";	[:te st none]; :set $endKey false;:set $keyFlag false}  
   :if ($endCheck) do={
    :if ($readString ~ $endCheckRegex) do={:set $endCheck false} else={[:te cuu;:te st varname]; :put "$2 $display \t Error: format of entry is incorrect. See example in help underneath.$sound"; [:te st none]; :set $keyFlag false; :set $endKey false} }; # :if ($endcheck..
  } else={:set $upKey true}; # Skip the checks if change up to an other entry. $readString should be destroyed and ignored on return.
 :set $endKey false
 } while ((([:len $readString] < $minLength) || $endCheck) && $i != 60931 );  #while check minium
 #:log info "$upKey $i"
 :return {$readString;$display;$upKey}
}; # End of function readKeyString, the value where returned to the caller, saved there in an array

#####################################################################################################################
# defining variables and assign to them values if needed.
#:local readKeyString; # needed if is called to the :global
:global arrayResult   [:toarray ""]; :local arrayLabel [:toarray ""]; :local arrayControl [:toarray ""]; :global arrayDisplay [:toarray ""]
:set $keyString "123456789abcdefghijklmnopqrstuvwxyz"; # Limits the number of menu items and acts a sequencer of the menuitems.
# Store menu in an array
# The number/letters defines the sequence of in the menu, description/help and then the show menu item with always a ":".
# If needed a minimal and maximal length that should be entered and then control characters:
# @ = e-mail addres, / = web/url, # = numbers and * = should not be displayed 
:set $arrayLabel { "7 IPv4 address 192.168.0.1"="IPv4 address:I";
		   "1 Login name which you want to use for this device."="Login name:1-15";
                   "2 Enter or change password."="Your password:4-8*";
                   "3 Pin"="Pin:4-4#*";
                   "4 Remark"="Remark 2:"
                   "z Are all entries correct. If not use ARROW-UP to correct. Then press ENTER."="Ready:"}
                   #"5 Webpage Example: (www.)mikrotik.com"="Webpage:/";
                   #"6 e-mail address (example: you@mail.com)"="E-mail:@"

# The control characters used in $arrayLabel should be present in $arraControl. Manual entry needed in the key requests and error messages.                   
 :set $arrayControl {"/";"@";"#";"*";"I";"P"}; # URL e-mail number hidden	#"8 IPv6 address 192.168.0.1"="IPv4 address:P";}                  
# Fill the support array with their assinged values.
 :for i from=0 to=([:len $arrayLabel]-1) do={:set $a [:pick $arrayLabel $i];:set $a "$[:pick $keyString $i ($i+1)]$[:pick $a 0 ([:find $a ":"]+1)]";:set ($arrayDisplay->"$a") ""}; # Fill $arraydisplay with labels
 :for i from=0 to=([:len $arrayDisplay]-1) do={:set $h $i; :set $a [:tostr [:pick $arrayDisplay $i ($i+1)]]; :set $a [:pick $a 0 [:find $a "="]]}
 :set $count 0; # set to default value                 
# Prints the header of the menue
 :put "- Menu testing -"
 :put "----------------"
# Writes out the full menu and then return to the first line of th menu
 :foreach label,value in $arrayDisplay do={:put "$[:pick $label 1 [:len $label]] $value"};	# Shows the whole menu.
 :for i from=0 to=([:len $arrayDisplay]-1) do={:te cuu};	                                # Goes to the top of the shown menu. 
# Starting the calls to the readkey function.
:while ($count <= ([:len $arrayLabel]-1)) do={

:set $a [:tostr [:pick $arrayDisplay $Count ($count+1)]]; :set $label [:pick $a 1 [:find $a "="]]; # Gets the $label of the next menu-item and cleans it up.
:if (($returnArray->2) && $count = 0) do={} else={:put $label}; # Only display the label if it is a first display
:set $label	[:pic $arrayLabel $count]; # Gets the $label
:set $descriptLabel [:tostr [:pick $arrayLabel $count ($count+1)]];	# gets the array string including the key.
:set $descript [:pick $descriptLabel 2 [:find $descriptLabel "="]];	# Get key of array...why is that made so difficult. Also removes the first two index characters in one go.

# getting control characters and this can be simplified by adding a extra parameter to the called function
 :set $length;
 :set $control [:pick $label ([:find $label ":"]+1) [:len $label]]
 :set $label [:pick $label 0 ([:find $label ":"]+1)]; # restore the label without the number for length
 :foreach findControl in=$arrayControl do={
  :foreach findControl in=$arrayControl do={:if ([:typeof [:find $control $findControl ([:len $control]-2)]] != "nil") do={:set $control [:pick $control 0 ([:len $control]-1)]; :set $label "$label$findControl"}} 
 }; # nibling control away, to obtain only the $length and add at the same time those to $label
 :set $length $control; # The leftover is the lenght if defined.
 :set $control; # Erase $control
 
#:put "Label: $label / control: $control / length: $length / descript: $descript \n\n\n\n"
#:error ""

#:set $descript [:pick $descript 2 ([:len $descript])]; # Remove the first two characters of the string. These are only used to order the fields in the array.
 :if ([:len $length] = 0) do={:set $length "0-255"}; # Always set a length even if it is zero zero
#:log info "count: $count / return: $($returnArray->2)"
# Call to function
#:set $show "PassWord"
 :set $returnArray [$readKeyString $descript $label $length [:pick $arrayDisplay $count] [:pick $arrayResult $count]]; 	# Call to function
 :if (($returnArray->2) && $count > 0) do={[:te el]; :put "$[:pick $label 0 ([:find $label :]+1)] $[:pick $arrayDisplay $count]";[:te cuu;:te cuu];:set $count (count-1); :if ($count > 0) do={[:te cuu]} } else={
   :set $resultString	($returnArray->0);:set $displayString	($returnArray->1); # Get the result and display values from the array 
# put a number/letter from $keyString in front of the label so that sequence in which the array stays as entered. (no auto sort)
   :set $label	"$[:pick $keyString $count ($count+1)]$label";
   :set $label	[:pick $label 0 ([:find $label ":"]+1)]; # Remove any returned control characters
# store the returned entries and displayed
   :set ($arrayResult->"$label") $resultString
   :set ($arrayDisplay->"$label") $displayString
   :if ($returnArray->2) do={} else={:set $count ($count + 1)}; # Add one to the counter in While if the entry was ignored and the the arrow up key was pressed
 }; # On $upKey all the not confirmed by and ENTER are ignored and the previous entry is selected
};

# And show the entered values
 [:te style syntax-no]; :put "Returned entries:"
 :foreach label,value in=$arrayResult do={
  [:te st syntax-no];   :put "$[:pick $label 1 ([:find $label ":"]+1)] ------------------";	# "1" Removes the sort letter and the "+1" keeps the ":".
  [:te cuu;:te st esc]; :put "\t\t $value                                                "
  :terminal style none
 }; # foreach label,string
 :put "\n"
}

Have fun :wink: