Gabriel Keess
Gabriel Keess

Reputation: 31

How to make a series of toggle switches in a scrollable menu with Batch?

I have spent hours doing this.

@echo off
set list = 0 0 1 1
:loop
cls
echo Program Select
echo --------------
set "el=0"
for %%a in (%list%) do ( 
    set /a "el+=1"
    if %%a equ 0 echo "[ ] Program %el%"
    if %%a equ 1 echo "[X] Program %el%"
)
echo ----------------------------------------------------
echo W = Up  /  S = Down  /  L  = Toggle  /  H  = Confirm
choice /C WSLH /N >nul
if %ERRORLEVEL% equ 1 set key=UP
if %ERRORLEVEL% equ 2 set key=DN
if %ERRORLEVEL% equ 3 set key=SL
if %ERRORLEVEL% equ 4 set key=CN
echo %key%
pause >nul
goto loop

Now the key variable works fine, and I have yet to implement scrolling, because I can't seem to get it to even render the text.

The goal is to get output like this

Program Select
--------------
[ ] Program 1
[ ] Program 2
[X] Program 3
[X] Program 4
----------------------------------------------------
W = Up  /  S = Down  /  L  = Toggle  /  H  = Confirm

But instead, I just get the Program Select and the controls. What am I missing?

Upvotes: 3

Views: 288

Answers (2)

T3RR0R
T3RR0R

Reputation: 2951

I can understand the desire to format the display of menu's - however I wouldn't recommend the approach your considering. Making a user scroll to the option they want and then confirm it isn't making the script easier for the user.

It is much simpler for a user to select an option from the list with a single keypress. You can easily add a confirmation after the selection where it's critical to confirm an action.

I've developed a template for menu's (For Windows 10) that allows for easy scripting and action of menu options if you'd like to give it a go. A practical GUI script that uses the template is exampled here

Something that's more in line with the output you want that's done in a simpler way:


Update - Backwards Compatable Version:

Enhanced features:

  • Array for menu options supports 36 choice options total
  • Color Mode Selection - Colors may now be disabled at runtime.
  • Findstr color output for legacy OS converted to a macro for faster execution.
::: Multi-Use Menu :: Author - T3RRY ::  Version 4.2 :: October 2020 ::
::: %Menu% macro - accepts up to 36 options for selection as quoted Args in list form.
::: parameters to call functions with via a given menu can be supplied using Substring modification as exampled.
::: - Multiple selection of menu items achieved using loops.
::: Tests if a menu option is a label and calls if true.
::: Builds a list of selected options [ that can be deselected ] when used in a loop.
@Echo off & Goto :Main
:::::::::::::::::::::::::: [* Balances Environment stack on script completion *]
:End [Endlocal ^& Set "_End=Y" ^& Exit /B 0]
 Color 07 & %CLOSE%
:::::::::::::::::::::::::::::::::::::::::::::: Findstr based Colorprint function
::: No longer used within menu macro
::: Unsupported Chars: "<" ">" ":" "\" "/" "?" "&"
:C_out [BG:a-f|0-9][FG:a-f|0-9] ["Quoted Strings" "to print"]
 Set "Str_=%*" &  Set "_Str=" & For %%G in (!Str_!)Do Set "_Str=!_Str! %%~G"
 Set "C_Out=%~1"
 Set "_Str=!_Str:%1 =!" & For /F "Delims=" %%G in ("!_Str!")Do Set "_Str=%%~G"
 For %%G in (!_Str!) Do Set ".Str=%%G"
  If /I "!.Str!" == "Exit"  (Set "C_Out=04") Else If /I "!.Str!" == "Next" (Set "C_Out=02") Else If /I "!.Str!" == "Continue" (Set "C_Out=02") Else If /I "!.Str!" == "Back" (Set "C_Out=05") Else If /I "!.Str!" == "Return" (Set "C_Out=05")
    <nul set /p ".=%DEL%" > " !_Str!"
    findstr /v /a:!C_Out! /R "^$" " !_Str!" nul
    del " !_Str!" > nul 2>&1
    Echo/
Exit /B 0
::::::::::::::::::::::::::::::::::::::::::::::::::::::::: Key variable and Macro definition
:main
::::::::::::::::::::: [ For readablities sake - %Menu% macro is built from the following ]:
rem ::: Order of definition must be preserved.
rem [* prepare default findstr color for non windows 10 users *]
 For /F "tokens=1,2 delims=#" %%a in ('"prompt #$H#$E# & echo on & for %%b in (1) do rem"') do (set "DEL=%%a")
rem [* default color args for Picked / Not Picked options. Overriden to Echo with Ascii Escape codes if Windows 10 *]
rem [* Color /? for the full combination of colors - BG + FG colors must differ. [BG:a-f|0-9][FG:a-f|0-9] *]
 Set "ColText=For %%l in (1 2)Do if %%l==2 (Set "_Str="&(Set "C_Out=!Oline:~0,2!" & Set "_Str=!Oline:~3!")&(For %%s in (!_Str!)Do Set ".Str=%%s")&(If /I "!.Str!" == "Exit"  (Set "C_Out=04") Else If /I "!.Str!" == "Next" (Set "C_Out=02") Else If /I "!.Str!" == "Continue" (Set "C_Out=02") Else If /I "!.Str!" == "Back" (Set "C_Out=05") Else If /I "!.Str!" == "Return" (Set "C_Out=05"))&( <nul set /p ".=%DEL%" > " !_Str!" )&( findstr /v /a:!C_Out! /R "^$" " !_Str!" nul )&( del " !_Str!" > nul 2>&1 )& Echo/)Else Set Oline="
 Set "_End="

:# Windows Version control. Assigns flag true if system is windows 10 build GTR 10586
:# https://en.wikipedia.org/wiki/ANSI_escape_code#DOS,_OS/2,_and_Windows
:# Version 1511 build number = 10.0.10586
 Set "Win10="
 For /f "tokens=3 delims=." %%v in ('Ver')Do if %%v GTR 10586 Set "Win10=True"

:# If Win10 true ; Test if virtual terminal codes enabled ; enable if false
:# removes win10 flag definition if version does not support Virtual Terminal sequences
:# Reg values: https://devblogs.microsoft.com/commandline/understanding-windows-console-host-settings/
 If defined Win10 (
  Reg Query HKCU\Console | %SystemRoot%\System32\findstr.exe /LIC:"VirtualTerminalLevel    REG_DWORD    0x1" > nul || (
    Reg Add HKCU\Console /f /v VirtualTerminalLevel /t REG_DWORD /d 1
  ) > Nul && (
    Echo(CMD restart required to enable Virtual terminal sequences.
    Pause
    EXIT
  ) || Set "Win10="
 )

  If defined Win10 For /f %%e in ('Echo(prompt $E^|Cmd') Do Set "\E=%%e"

  If Defined Win10 (
   Set "_nP=Echo/%\E%[90m"& Set "_P=Echo/%\E%[33m"
   Echo/Menu Color mode: [L]egacy [W]indows [D]isabled & For /F "Delims=" %%C in (' Choice /N /C:LWD ')Do (
    If "%%C" =="L" (
     Set "_nP=For %%l in (1 2)Do if %%l==2 (Set "_Str="&(Set "C_Out=!Oline:~0,2!" & Set "_Str=!Oline:~3!")&(For %%s in (!_Str!)Do Set ".Str=%%s")&(If /I "!.Str!" == "Exit"  (Set "C_Out=04") Else If /I "!.Str!" == "Next" (Set "C_Out=02") Else If /I "!.Str!" == "Continue" (Set "C_Out=02") Else If /I "!.Str!" == "Back" (Set "C_Out=05") Else If /I "!.Str!" == "Return" (Set "C_Out=05"))&( <nul set /p ".=%DEL%" > " !_Str!" )&( findstr /v /a:!C_Out! /R "^$" " !_Str!" nul )&( del " !_Str!" > nul 2>&1 )& Echo/)Else Set Oline=08"
     Set "_P=For %%l in (1 2)Do if %%l==2 (Set "_Str="&(Set "C_Out=!Oline:~0,2!" & Set "_Str=!Oline:~3!")&(For %%s in (!_Str!)Do Set ".Str=%%s")&(If /I "!.Str!" == "Exit"  (Set "C_Out=04") Else If /I "!.Str!" == "Next" (Set "C_Out=02") Else If /I "!.Str!" == "Continue" (Set "C_Out=02") Else If /I "!.Str!" == "Back" (Set "C_Out=05") Else If /I "!.Str!" == "Return" (Set "C_Out=05"))&( <nul set /p ".=%DEL%" > " !_Str!" )&( findstr /v /a:!C_Out! /R "^$" " !_Str!" nul )&( del " !_Str!" > nul 2>&1 )& Echo/)Else Set Oline=06"
    )
    If "%%C" =="D" (Set "_nP=Echo/"& Set "_P=Echo/")
   )
  ) Else (
    Set "_nP=For %%l in (1 2)Do if %%l==2 (Set "_Str="&(Set "C_Out=!Oline:~0,2!" & Set "_Str=!Oline:~3!")&(For %%s in (!_Str!)Do Set ".Str=%%s")&(If /I "!.Str!" == "Exit"  (Set "C_Out=04") Else If /I "!.Str!" == "Next" (Set "C_Out=02") Else If /I "!.Str!" == "Continue" (Set "C_Out=02") Else If /I "!.Str!" == "Back" (Set "C_Out=05") Else If /I "!.Str!" == "Return" (Set "C_Out=05"))&( <nul set /p ".=%DEL%" > " !_Str!" )&( findstr /v /a:!C_Out! /R "^$" " !_Str!" nul )&( del " !_Str!" > nul 2>&1 )& Echo/)Else Set Oline=08"
    Set "_P=For %%l in (1 2)Do if %%l==2 (Set "_Str="&(Set "C_Out=!Oline:~0,2!" & Set "_Str=!Oline:~3!")&(For %%s in (!_Str!)Do Set ".Str=%%s")&(If /I "!.Str!" == "Exit"  (Set "C_Out=04") Else If /I "!.Str!" == "Next" (Set "C_Out=02") Else If /I "!.Str!" == "Continue" (Set "C_Out=02") Else If /I "!.Str!" == "Back" (Set "C_Out=05") Else If /I "!.Str!" == "Return" (Set "C_Out=05"))&( <nul set /p ".=%DEL%" > " !_Str!" )&( findstr /v /a:!C_Out! /R "^$" " !_Str!" nul )&( del " !_Str!" > nul 2>&1 )& Echo/)Else Set Oline=06"
    Echo/Menu Color mode: [L]egacy [D]isabled & For /F "Delims=" %%C in (' Choice /N /C:LD ')Do If "%%C" =="D" (Set "_nP=Echo/"& Set "_P=Echo/")
   )
  )

rem [* Menu supports 36 choices using _O array index with substring modification on _Cho var to index choice selection of Array Elements *]
 Set "_Cho=0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
 Set "DisplayArray=(Echo/!#Sel!| Findstr /LIC:"%%~G" > Nul 2> Nul && ( %_P% [!_Cho:~%%z,1!] X %%~G ) ) || ( %_nP% [!_Cho:~%%z,1!] - %%~G )"
 Set "#Sel=_Nil"
 Set "ClearArray=(For /F "Tokens=1,2 Delims==" %%i in (' Set Opt[ 2^> Nul ')Do "Set %%i=")"
 Set "ResetVars=(Set "#L=F" &  Set "OptL=" & Set "_O=0")"
 Set "CLOSE=POPD & Endlocal & Set "_End=Y" & Exit /B 0"
 Set "BuildArray=((If !_O! GTR 35 (Call :C_Out 04 "Maximum options [!_O!] Exceeded" & (Timeout /T 5 /NOBREAK) & %CLOSE%))&Set "OptL=!OptL!!_Cho:~%%z,1!"&Set "Opt[!_Cho:~%%z,1!]=%%~G")"
 Set "MakeChoice=(For /F "Delims=" %%C in ('Choice /N /C:!OptL!')Do findstr.exe /BLIC:":!Opt[%%C]!" "%~F0" > nul 2> nul && Call :!Opt[%%C]! "Param" 2> Nul || ((Echo/"!#Sel!"| Findstr /LIC:"!Opt[%%C]!" > Nul 2> Nul && (For /F "Delims=" %%r in ("!Opt[%%C]!")Do If Not "!#Sel!" == "" (Set "#Sel=!#Sel:"%%r"=!")Else (Set "#Sel=_Nil"))) || (Set "#Sel=!#Sel! "!Opt[%%C]!"")))"
 Set "Return=For /L %%s in (0 1 4)Do (If not "!#Sel!" == "" (If "!#Sel:~0,1!" == " " (If "!#L!" == "F" (Set "#Sel=!#Sel:~1!"))Else (Set "#L=T"))Else (Set "#Sel=_Nil"))&if not "!#Sel!" == "_Nil" if not "!#Sel!" == "" (Set "#Sel=!#Sel:_Nil=!")"
 Set "Menu=(If defined _End Goto :End) &For %%n in (1 2)Do if %%n==2 (%ClearArray% & %ResetVars% &(For %%G in (!Options!)Do For %%z in (!_O!)Do %BuildArray% & %DisplayArray% &Set /A "_O+=1")& %MakeChoice% & %Return% )Else Set Options="
 For %%M in ( ClearArray ResetVars BuildArray DisplayArray MakeChoice Return )Do Set "%%M="
 IF NOT EXIST "%TEMP%\colorize" md "%TEMP%\colorize"
 PUSHD "%TEMP%\colorize" || (Echo/"%TEMP%\colorize" Could not be found & %CLOSE%)
:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
 Setlocal EnableExtensions EnableDelayedExpansion & REM [* required to be activated AFTER definition of Macro's. *]
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: Commence script main body | Demonstration of usages
:Loop
rem [* doublequoting of options recommended - Required for strings containing standard delimiters. *]
 If not defined _End cls
 If not defined _End %ColText%05 Make a selection.
rem [* Param Substring modification examples passing parameters to labels called when selected - Optional feature. *]
 Set /A "PRM=!Random! %%100 + 100" & Rem [* Example param only *]
 %Menu% "Exit" "Next" "Option 1" "Option 2" "Option 3" "Option 4"
 Echo/"!#Sel!" | Findstr.exe /LIC:"Exit" > Nul 2> Nul && (Goto :End)
Goto :Loop
 
:Next
rem [* Selection of a single option occurs by using the macro without a loop or resetting the #Sel variable
rem - between %menu% use and next iteration of a loop *]
rem [* Process #Sel items using the !#Sel! variable - then ** SET "#Sel-_Nil" prior to next usage of Menu macro** *]
 Set "Menu1Opts=!#Sel!"
 Set "#Sel="
 Cls
 Call :C_Out 03 "Selected =" !Menu1opts!
 %Menu% "Exit" " + Continue" ".. Back"
 Echo/!#Sel! | Findstr.exe /LIC:".. Back" > Nul 2> Nul && (Set "#Sel=!Menu1opts!"& Exit /B 0)
 Echo/!#Sel! | Findstr.exe /LIC:"Exit" > Nul 2> Nul && (%CLOSE%)
 Set "#Sel="
 Echo/!Menu1opts! | Findstr.exe /LIC:"_Nil" > Nul 2> Nul && (Call :C_Out 04 "Selection Required."&(Pause & Exit /B 0)) || Call :C_Out 02 "Confirmed=" !Menu1opts!
 Call :C_Out 02 "Example complete."
 Pause
rem [* to exit at end of script or by user selection *]
 %CLOSE%
:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: End of example

Output:

selection differentiation

Upvotes: 1

Magoo
Magoo

Reputation: 80138

Here's a more "meated-out" version of your code:

@echo off
SETLOCAL enabledelayedexpansion


:: Program names used

set "program[1]=Program one"
set "program[2]=Program two"
set "program[3]=Program three"
set "program[4]=Program four"
set /a maxprogs=4

:: symbols used for statuses

set "symbols= X"
set "symbolsc=-+"

:restart
:: Set original list status. 0=not selected, 1=selected;

set "list=0 0 1 1"
set /a cursor=1

:loop
cls
echo Program Select
echo --------------
set /a el=0
for %%a in (%list%) do ( 
    set /a el+=1
    if !el!==%cursor% (set "ds=!symbolsc:~%%a,1!") else (set "ds=!symbols:~%%a,1!")
    call set "progname=%%program[!el!]%%"
    echo [!ds!] !progname!
)
echo ----------------------------------------------------
choice /C WSLHQ /N /M "W = Up  /  S = Down  /  L  = Toggle  /  H  = Confirm / Q = Quit "
set /a key=%errorlevel%
if %key%==5 goto :eof
if %key%==4 goto confirm
if %key%==3 goto toggle
if %key%==2 if %cursor%==%maxprogs% (set /a cursor=1) else set /a cursor+=1
if %key%==1 if %cursor%==1 (set /a cursor=%maxprogs%) else set /a cursor-=1

goto loop

:confirm
echo Confirmed!
set "runany="
set /a el=0
for %%a in (%list%) do ( 
    set /a el+=1
    if %%a==1 (
     set /a runany+=1
     call set "progname=%%program[!el!]%%"
     echo Run !progname!
    )
)
if not defined runany echo None selected :(

timeout /t 5 /nobreak
goto restart

:toggle
set "newlist="
set /a el=0
for %%a in (%list%) do ( 
    set /a el+=1
    if !el!==%cursor% (
     if %%a==0 (set "newlist=!newlist! 1") else (set "newlist=!newlist! 0")
    ) else set "newlist=!newlist! %%a"
)
set "list=%newlist%"
goto loop

Comments:

SETLOCAL enabledelayedexpansion allows !var! to access the changed value of var within a loop (%var% accesses the original value, before the loop started executing).

I used maxprogs so that expansion of the list is intuitive - just follow the bouncing ball...

Since the cursor is static, I used symbols to represent the non-selected/selected states and symbolsc for when the "cursor" is on the state, so + is cursor-is-here-it's-selected and - is cursor-is-here-it's-not-selected

The list usage is similar to your version - cursor is for the current cursor-line.

In the display-selections-and-prognames section, note the use of !el! to access the modified value of el within the loop

The tricky bit here is the call set "progname=%%program[!el!]%%" statement. This uses a parsing trick to get the value of the program name el. Assuming the current value of el is 2 for instance, this executes the command set "progname=%program[2]%" in a sub-shell by interpreting %% as an escaped-% and substituting for the current value of el. The subshell inherits the environment of its caller, so the destination variable is assigned from the calculated value.

I've modified the choice command to prompt with the legend, and added Q for Quit as good measure. I'd have used UDTRQ myself, for Up/Down/Toggle/Run/Quit, but something tells me you may not necessarily be using English.

I set errorlevel into key to avoid having to be particularly careful about maintaining its value, then tested key for the only 5 values of interest; on others just beep (from choice and present-again.

Quit is obvious; cursor-move simply increments or decrements cursor and checks boundary conditions for roll-around.

The other two routines are simply modifications of the display-list routine; the run displays the prognames as I have no idea whether you want to run the programs serially or in parallel.

The toggle routine uses the same technique to rebuild list whilst toggling the cursor'th element.

Upvotes: 3

Related Questions