Query States using Console Virtual Terminal Sequences.

Discussion forum for all Windows batch related topics.

Moderator: DosItHelp

Post Reply
Message
Author
penpen
Expert
Posts: 1786
Joined: 23 Jun 2013 06:15
Location: Germany

Query States using Console Virtual Terminal Sequences.

#1 Post by penpen » 15 Feb 2020 12:45

The "Query State"-functions in vt101 escape sequences (if activated in win 10 console) emit their responses into the console input stream (without injecting "key-presses") and are therefore hard to read.

I think it can't be done using pure batch (or i haven't found a way yet).
You could use jscript to insert a return input (WshShell.SendKeys("{ENTER}");) and read the result (while (!WScript.StdIn.AtEndOfLine) { input += WScript.StdIn.Read(1);}). But those i/o functions are all high level iostream routines, which results in an unwanted echo of the report on the screen.

The only solution i saw is using the (c++) low level routines "_kbhit()" and "_getwch()" invoked by a c# script.
While the report-device-attributes actually aren't really needed (Microsoft provided "VT101 with No Options" only, but in theory you could use additional software to add VT400 handling to console), the report-cursor-position might be more usefull if you want to create batches using vt100 sequences and displayed user input, that must be protected from beeing overwritten,

Here is a sample "report.vt101.bat" (which creates "fibto.exe" from c# code; tested on Win 10.0.18362.657):

Code: Select all

@echo off
setlocal enableExtensions disableDelayedExpansion
if not exist "fibto.exe" call :init
if exist "fibto.pdb" del "fibto.pdb"
for /f %%a in ('echo prompt $E^| cmd') do set "ESC=%%a"

call :getCursorPosition "$cursorPos1"
call :getDeviceAttributes "$deviceAttrib"
set "$"
<nul set /p "=__________"
call :getCursorPosition "$cursorPos2"
set "$cursorPos2"
goto :eof


:: https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences#query-state
:: Report Cursor Position  : %ESC%[6n
:getCursorPosition
:: %~1  environment variable name to store the result into
if "%~1" == "" goto :eof
set "%~1="
<nul set /p "=%ESC%[6n"
for /f "tokens=* delims=" %%a in ('"fibto.exe"') do set "%~1=%%~a"
goto :eof

:: https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences#query-state
:: Report Device Attributes: %ESC%[0c
:getDeviceAttributes
:: %~1  environment variable name to store the result into
if "%~1" == "" goto :eof
set "%~1="
<nul set /p "=%ESC%[0c"
for /f "tokens=* delims=" %%a in ('"fibto.exe"') do set "%~1=%%~a"
goto :eof


:compile
for /f "tokens=1 delims=:" %%a in ('findstr  /o /c:"%~1" "%~f0"') do set /a "offset=%%~a"
for /f "tokens=1 delims=:" %%a in ('findstr  /n /r /c:"^.Source = @" "%~f0"') do set /a "sourceOffset=%%~a"
for %%a in ("%ComSpec%") do set "ps.exe=%%~dpaWindowsPowerShell\v1.0\powershell.exe"
if not exist "%ps.exe%" exit /b 2 ERROR_FILE_NOT_FOUND
call rem init ERROR_SUCCESS
"%ps.exe%" "-Command" "$SourceOffset = "%sourceOffset%"; Invoke-Expression $([System.IO.File]::ReadAllText('%~f0').substring(%offset%))"
del
exit /b %errorLevel%

:init
call :compile ^
#powershell
<# <nul exit /b %errorLevel%
	hybrid batch function/powershell comment
#>
Function Get-CurrentLine {
    $Myinvocation.ScriptlineNumber
}

$CompilerAssemblies = (
	"System",
	"Microsoft.CSharp"
)

$CompilerSource = @"
using System;
using System.Collections.Generic;
using Microsoft.CSharp;
using System.CodeDom.Compiler;

namespace Penpen {
	public static class Compiler {
		public static void Compile(string[] assemblies, string source, string mainClass, string exeFileName) {
			var options = new Dictionary<string, string> { { "CompilerVersion", "v4.0" } };
			CSharpCodeProvider compiler = new CSharpCodeProvider(options);
			System.CodeDom.Compiler.CompilerParameters parameters = new CompilerParameters();

			parameters.OutputAssembly = exeFileName;
			if (mainClass != null) {
				parameters.MainClass = mainClass;		// set main entry point of the executable
			}
			if (assemblies != null) {
				for (int i = 0; i < assemblies.Length; ++i) {
					parameters.ReferencedAssemblies.Add(assemblies[i] + ".dll");
				}
			}
			parameters.GenerateExecutable = true;		// true: generate exe; false: generate dll
			parameters.GenerateInMemory = false;		// true: memory; false: disk
			parameters.IncludeDebugInformation = true;	// true: debug; false: release
			parameters.WarningLevel = 4;			// 0: no; 1: severe; 2: +normal; 3: +less severe; 4(default): +informational
			parameters.TreatWarningsAsErrors = true;

			CompilerResults results = compiler.CompileAssemblyFromSource(parameters, source);
			if (results.Errors.Count > 0) {
				foreach(CompilerError CompErr in results.Errors) {
					Console.WriteLine(
						"Line number {0}, Error Number: {1}, '{2}';\r\n",
						(CompErr.Line + 
"@
$CompilerSource += $sourceOffset
$CompilerSource += @"
),
						CompErr.ErrorNumber,
						CompErr.ErrorText
					);
				}
			}
		}
	}
}
"@

$Assemblies = (
	"System",
	"System.Runtime.InteropServices"
)

$Source = @"
using System;
using System.Runtime.InteropServices;

using wchar_t = System.Char;

namespace Penpen {
	public static class FlushInBuffToOut {
		[DllImport("msvcrt.dll", CallingConvention=CallingConvention.Cdecl)]
		static extern int _kbhit();

		[DllImport("msvcrt.dll", CallingConvention=CallingConvention.Cdecl)]
		static extern wchar_t _getwch();


		public const wchar_t ESC = '\u001B';


		public static void Main() {
			wchar_t wch;

			while (_kbhit() != 0) {
				wch = _getwch();

				if (wch == ESC) {
					Console.Write("{0}", "{ESC}");
				} else {
					Console.Write("{0}", wch);
				}
			}
		}
	}
}
"@

#Add-Type -ReferencedAssemblies $Assemblies -TypeDefinition $Source -Language CSharp  
#[Penpen.FlushInBuffToOut]::Main()

Add-Type -ReferencedAssemblies $CompilerAssemblies -TypeDefinition $CompilerSource -Language CSharp
[Penpen.Compiler]::Compile($Assemblies, $Source, "Penpen.FlushInBuffToOut", "fibto.exe")

penpen

jeb
Expert
Posts: 950
Joined: 30 Aug 2007 08:05
Location: Germany, Bochum

Re: Query States using Console Virtual Terminal Sequences.

#2 Post by jeb » 17 Feb 2020 00:40

Hi penpen,
penpen wrote:
15 Feb 2020 12:45
I think it can't be done using pure batch (or i haven't found a way yet).
Yes, it can 8)

Code: Select all

@echo off
for /F "delims=#" %%a in ('"prompt #$E# & for %%a in (1) do rem"') do set "ESC=%%a"

call :get_cursor_pos pos
exit /b

:get_cursor_pos
set "response="
set pos=2

:_get_loop
<nul set /p "=%ESC%[6n" 
FOR /L %%# in (1 1 %pos%) DO pause < CON > NUL

for /F "tokens=2 delims=)" %%C in ('"xcopy /L /W ? ? < con"') DO (
	set "char=%%C"
)
set "response=%response%%char%"
set /a pos+=1
if "%char%" NEQ "R" goto :_get_loop

set response
exit /b
Currently this solution is a bit nasty, it uses XCOPY to read a single character from the response,
but XCOPY has the bad side effect of flushing the complete input buffer.
Therefor, I request the cursor position response multiple times and read only one character in each loop until I get the "R" as last char.
The prefix characters are dropped by the PAUSE command.
PAUSE read only a single character, but can't be used to fetch them, as it doesn't output the read character. :cry:

I tried REPLACE, it works too, but it flushes like XCOPY.
CHOICE can't read from CON at all, it only throws an error: `Access is denied`.
"set /p var= < CON" fetches all characters, BUT waits until ENTER is pressed and I don't know how to inject/simulate it.
type CON | findstr /N "^" --- works, but waits until ENTER and CTRL-Z is pressed, worse than set/p

jeb

penpen
Expert
Posts: 1786
Joined: 23 Jun 2013 06:15
Location: Germany

Re: Query States using Console Virtual Terminal Sequences.

#3 Post by penpen » 17 Feb 2020 05:17

Your solution doesn't work on my system :cry: .

The output of my xcopy (if not used in a for/f-loop) for each character is:

Code: Select all

Eine beliebige Taste drücken, um das Kopieren der Datei(en) zu starten 5
0 Datei(en) kopiert
...
So i would have to undefine the environment variable "char" before the loop, add an "if not defined char" to the loop, and only store the last character.

But it is worse (at least on my system):
If i use a for/f-loop, the input characters are gone, the batch doesn't produce any output and i have to press and hold CTRL+C.

Code: Select all

Z:\>test.bat
Batchvorgang abbrechen (J/N)?
^CBatchvorgang abbrechen (J/N)?
^CDas System kann das angegebene Gerät oder die angegebene Datei nicht öffnen.
Drücken Sie eine beliebige Taste . . .
Batchvorgang abbrechen (J/N)?
^C
Z:\>
Your batch modified to match my xcopy ("test.bat"):

Code: Select all

@echo off
for /F "delims=#" %%a in ('"prompt #$E# & for %%a in (1) do rem"') do set "ESC=%%a"

call :get_cursor_pos pos
pause
exit /b

:get_cursor_pos
set "response="
set pos=2

:_get_loop
<nul set /p "=%ESC%[6n" 
FOR /L %%# in (1 1 %pos%) DO pause < CON > NUL

set "char="
for /F "tokens=2 delims=)" %%C in ('"xcopy /L /W ? ?"') DO (
	if not defined char set "char=%%C"
)
set "char=%char:~-1%"
set "response=%response%%char%"
set /a pos+=1
if "%char%" NEQ "R" goto :_get_loop

set response
exit /b

penpen

jeb
Expert
Posts: 950
Joined: 30 Aug 2007 08:05
Location: Germany, Bochum

Re: Query States using Console Virtual Terminal Sequences.

#4 Post by jeb » 17 Feb 2020 06:22

Probably I should have used REPLACE instead of XCOPY.
Replace echos the input character on a separate line

Then the fetch part is

Code: Select all

set "char=;"
for /F "tokens=1 skip=1 delims=*" %%C in ('"REPLACE /W ? . < con"') DO (
	set "char=%%C"
)

penpen
Expert
Posts: 1786
Joined: 23 Jun 2013 06:15
Location: Germany

Re: Query States using Console Virtual Terminal Sequences.

#5 Post by penpen » 19 Feb 2020 04:16

Your "replace.exe"-version works perfectly :D :

Code: Select all

@echo off
for /F "delims=#" %%a in ('"prompt #$E# & for %%a in (1) do rem"') do set "ESC=%%a"

call :get_cursor_pos pos
exit /b

:get_cursor_pos
set "response="
set pos=2

:_get_loop
<nul set /p "=%ESC%[6n" 
FOR /L %%# in (1 1 %pos%) DO pause < CON > NUL

set "char=;"
for /F "tokens=1 skip=1 delims=*" %%C in ('"REPLACE /W ? . < con"') DO (
	set "char=%%C"
)
set "response=%response%%char%"
set /a pos+=1
if "%char%" NEQ "R" goto :_get_loop

set response
exit /b
penpen

jeb
Expert
Posts: 950
Joined: 30 Aug 2007 08:05
Location: Germany, Bochum

Re: Query States using Console Virtual Terminal Sequences.

#6 Post by jeb » 19 Feb 2020 09:36

One more test to fetch the cursor report with an unexpected result 8) :!:

Code: Select all

@echo off
FOR /F "tokens=3 delims=:" %%L in ("%~0") DO goto :%%L

REM *** Save Cursor Position in Memory
<nul set /p "=%\e%7"

REM *** Bring the cursor to x,y=20,20
<nul set /p "=%\e%[20;20H"

REM *** Request Cursor report
<nul set /p "=%\e%[6n"

REM *** Restore Cursor Position from Memory
<nul set /p "=%\e%8"

REM *** Avoids storing the input into the history buffer
call %~d0\:main:\..\%~pnx0 | break
exit /b

:main
for /F "delims=#" %%a in ('"prompt #$E# & for %%a in (1) do rem"') do set "\e=%%a"

REM *** Doskey doesn't support quoted syntax
doskey %\e%[20;20R=This is a doskey macro, unexpected eh?

REM *** Read the cursor report
set "var=[unset]"
<CON >CON set /p "var=Press enter - NOW! "

echo The content of var is "%var%" > CON

jeb

dbenham
Expert
Posts: 2347
Joined: 12 Feb 2011 21:02
Location: United States (east coast)

Re: Query States using Console Virtual Terminal Sequences.

#7 Post by dbenham » 19 Feb 2020 11:30

:shock: Holy smokes! Nice work!

I never expected to see a solution for reading the cursor position with pure batch. I can see this being genuinely useful, although I wish it were faster. You should update your old answer at https://stackoverflow.com/a/38240300/1012053 so I can then accept it. :)

Nor did I ever expect pure batch to ever execute a macro. Although I had to move the definition of \e to the top of the script to get it to work. This is a nice parlor trick, but I can't imagine how it could ever be useful.

I took a stab at making a fully functional utility function for getting the cursor position. I substituted FOR /L for the GOTO loop - it should support row and column values up to 99999.

Code: Select all

:get_cursor_pos  rtnObj
::
:: If rtnObj is specified, then returns the current cursor position as
::   rtnObj.x = column position (1 = left most column)
::   rtnObj.y = row position (1 = top most row)
:: else writes the current cursor position as {row;col} to the screen.
::
:: Also defines \e to contain an escape character if it is not already defined.
::
if not defined \e for /f "delims=#" %%E in ('"prompt #$E# & for %%a in (1) do rem"') do set "\e=%%E"
setlocal enableDelayedExpansion
set "response="
for /l %%N in (2 1 13) do (
  <nul set /p "=%\e%[6n"
  for /l %%# in (1 1 %%N) do pause < con > nul
  for /f "skip=1 eol=" %%C in ('"replace /w ? . < con"') do (
    if "%%C"=="R" (
      if "%~1" neq "" (
        for /f "delims=; tokens=1,2" %%A in ("!response!") do (
          endlocal
          set "%~1.y=%%A"
          set "%~1.x=%%B"
        )
      ) else echo {!response!}
      exit /b 0
    ) else set "response=!response!%%C"
  )
)
>&2 echo ERROR: get_cursor_pos failed to return a value
exit /b 1

Dave Benham

jeb
Expert
Posts: 950
Joined: 30 Aug 2007 08:05
Location: Germany, Bochum

Re: Query States using Console Virtual Terminal Sequences.

#8 Post by jeb » 19 Feb 2020 15:30

dbenham wrote:
19 Feb 2020 11:30
:shock: Holy smokes! Nice work!

I never expected to see a solution for reading the cursor position with pure batch. I can see this being genuinely useful, although I wish it were faster. You should update your old answer at https://stackoverflow.com/a/38240300/1012053 so I can then accept it. :)
Holy smoke, I didn't even remembered that I (partly) solved the same problem some years ago with nearly the same idea before :D
I updated the SO answer, please feel free to fix the spelling or grammar, I'm always pleased to learn from my mistakes.
dbenham wrote:
19 Feb 2020 11:30
Nor did I ever expect pure batch to ever execute a macro. ... This is a nice parlor trick, but I can't imagine how it could ever be useful.
I can't see any application for it, too.
But I hoped that it enables somehow set/p to read the response without waiting for an external ENTER.
Btw. The doskey macro expansion only works with set/p, I tested it also without success with copy /-y , xcopy and replace
dbenham wrote:
19 Feb 2020 11:30
Although I had to move the definition of \e to the top of the script to get it to work.
Aaarg, I had accidentally an already defined "\e " variable in my environment, before starting the batch.


jeb

dbenham
Expert
Posts: 2347
Joined: 12 Feb 2011 21:02
Location: United States (east coast)

Re: Query States using Console Virtual Terminal Sequences.

#9 Post by dbenham » 20 Feb 2020 08:15

I was worried that the result might be garbled if there was pre-existing content in the console input buffer when the cursor position query was initiated.

So I ran a test where I inserted a PING delay before the query, and then manually pressed some keys during the PING delay. I was pleasantly surprised that the position query cleared the input buffer before inserting the position response :D


Dave Benham

jeb
Expert
Posts: 950
Joined: 30 Aug 2007 08:05
Location: Germany, Bochum

Re: Query States using Console Virtual Terminal Sequences.

#10 Post by jeb » 20 Feb 2020 08:50

dbenham wrote:
20 Feb 2020 08:15
I was pleasantly surprised that the position query cleared the input buffer before inserting the position response
No, the position query doesn't clear the input buffer.
:shock: BUT it inserts the response at the beginning of the buffer instead of appending it :?:

Code: Select all

for /F "delims=#" %%a in ('"prompt #$E# & for %%a in (1) do rem"') do set "\e=%%a"
echo Press some keys, NOW!

REM *** Save Cursor Position in Memory
<nul set /p "=%\e%7"

REM *** Bring cursor to pos 20,20
<nul set /p "=%ESC%[2;2H"
<nul set /p "=%ESC%[6n"

<nul set /p "=%ESC%[3;3H"
<nul set /p "=%ESC%[6n"

REM *** Restore Cursor Position from Memory
<nul set /p "=%\e%8"
ping -n 3 localhost > NUL

<nul set /p "=%ESC%[4;4H"
<nul set /p "=%ESC%[6n"
REM *** Restore Cursor Position from Memory
<nul set /p "=%\e%8"

REM *** Restore Cursor Position from Memory
<nul set /p "=%\e%8"

set /p var=
echo ---------
setlocal EnableDelayedExpansion
echo READ: !var:%\e%=\e!
exit /b
Output wrote:Press some keys, NOW!
^[[4;4R^[[3;3R^[[2;2Rhello
---------
READ: \e[4;4R\e[3;3R\e[2;2Rhello
You see, the order of the cursor position responses are reversed :!:
And my "Hello", typed while the ping waits, was appended.

WHY oh Microsoft :cry: :?:
Perhaps the cause could be, that a program that want to get the screen position, doesn't need to handle ( and reinsert) user input

jeb
Expert
Posts: 950
Joined: 30 Aug 2007 08:05
Location: Germany, Bochum

Re: Query States using Console Virtual Terminal Sequences.

#11 Post by jeb » 20 Feb 2020 09:02

Tested on a linux system.

There the cpr is appended as expected and it has no other priority than user input.

dbenham
Expert
Posts: 2347
Joined: 12 Feb 2011 21:02
Location: United States (east coast)

Re: Query States using Console Virtual Terminal Sequences.

#12 Post by dbenham » 20 Feb 2020 10:48

Doh! I tested with the get_cursor_pos routine, and forgot that the necessary RENAME clears the input buffer after it reads a character. So I drew the wrong conclusion. :oops:

But I like the fact that the cursor position query inserts the response to the front of the input buffer :!: It means a program can perform the query and get the response without any interference from user input. Nor does it corrupt the user's input. I can't see any drawback.

dbenham
Expert
Posts: 2347
Joined: 12 Feb 2011 21:02
Location: United States (east coast)

Re: Query States using Console Virtual Terminal Sequences.

#13 Post by dbenham » 20 Feb 2020 12:02

It is not so simple to handle the cursor position query if the response is appended to the input queue. Your strategy of reinserting the user input after extracting the response could get complicated if the query is issued in the midst of a long barrage of user input. The response reader would have to read the entirety of the input buffer, extract the response (that might be in the middle), and then reinsert all of the input before any more input is received. That looks like a nasty race condition to me.

It is so much simpler when the response is inserted at the front of the input queue. The only complication with this is if the input buffer is full. But then that would be an issue regardless where the response is placed.

Post Reply