Over the past several weeks I've been working on and off on this. I didn't track the hours but I'm sure that I've spent 80+ hours on the feature. I've tested and retested all known scenarios and it seems to be working as expected.
From: Joe Souza jsouza@yahoo.com
--- programs/cmd/Makefile.in | 1 + programs/cmd/batch.c | 2 +- programs/cmd/lineedit.c | 534 +++++++++++++++++++++++++++++++++++++++ programs/cmd/wcmd.h | 1 + 4 files changed, 537 insertions(+), 1 deletion(-) create mode 100755 programs/cmd/lineedit.c
diff --git a/programs/cmd/Makefile.in b/programs/cmd/Makefile.in index cca40b84011..0c4c283505d 100644 --- a/programs/cmd/Makefile.in +++ b/programs/cmd/Makefile.in @@ -8,5 +8,6 @@ SOURCES = \ builtins.c \ cmd.rc \ directory.c \ + lineedit.c \ wcmd.svg \ wcmdmain.c diff --git a/programs/cmd/batch.c b/programs/cmd/batch.c index 20ae894dbfd..10881334e3e 100644 --- a/programs/cmd/batch.c +++ b/programs/cmd/batch.c @@ -227,7 +227,7 @@ WCHAR *WCMD_fgets(WCHAR *buf, DWORD noChars, HANDLE h) /* We can't use the native f* functions because of the filename syntax differences between DOS and Unix. Also need to lose the LF (or CRLF) from the line. */
- if (VerifyConsoleIoHandle(h) && ReadConsoleW(h, buf, noChars, &charsRead, NULL) && charsRead) { + if (VerifyConsoleIoHandle(h) && WCMD_ReadConsole(h, buf, noChars, &charsRead, NULL) && charsRead) { if (!charsRead) return NULL;
/* Find first EOL */ diff --git a/programs/cmd/lineedit.c b/programs/cmd/lineedit.c new file mode 100755 index 00000000000..a56551d5707 --- /dev/null +++ b/programs/cmd/lineedit.c @@ -0,0 +1,534 @@ +/* + * CMD - Wine-compatible command line interface - Command line editing + * functions, including tab-completion support + * + * Copyright (C) 2025 Joe Souza + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#define WIN32_LEAN_AND_MEAN + +#include "wcmd.h" +#include "wine/debug.h" + +WINE_DEFAULT_DEBUG_CHANNEL(cmd); + + +static int WriteCharacterToConsoleInput(const HANDLE hInput, const WCHAR inputChar) +{ + INPUT_RECORD inputRecord; + DWORD eventsWritten; + + /* Create an input record for a key press event */ + inputRecord.EventType = KEY_EVENT; + inputRecord.Event.KeyEvent.bKeyDown = TRUE; + inputRecord.Event.KeyEvent.wRepeatCount = 1; + inputRecord.Event.KeyEvent.wVirtualKeyCode = VkKeyScanW(inputChar); + inputRecord.Event.KeyEvent.wVirtualScanCode = MapVirtualKeyW(inputRecord.Event.KeyEvent.wVirtualKeyCode, MAPVK_VK_TO_VSC); + inputRecord.Event.KeyEvent.uChar.UnicodeChar = inputChar; + inputRecord.Event.KeyEvent.dwControlKeyState = 0; + + /* Write the input record to the console input buffer */ + if (!WriteConsoleInputW(hInput, &inputRecord, 1, &eventsWritten)) { + return 1; + } + + /* Create an input record for a key release event */ + inputRecord.Event.KeyEvent.bKeyDown = FALSE; + if (!WriteConsoleInputW(hInput, &inputRecord, 1, &eventsWritten)) { + return 1; + } + + return 0; +} + +static void WriteLineToConsoleInput(const HANDLE hInput, const WCHAR *inputBuffer, const ULONG inputBufferLength) +{ + int cc = 0; + + while (inputBuffer[cc] && cc < inputBufferLength) { + WriteCharacterToConsoleInput(hInput, inputBuffer[cc++]); + } +} + +static BOOL IsDirectoryOperation(const WCHAR *inputBuffer, const ULONG inputBufferLength) +{ + int cc = 0; + BOOL ret = FALSE; + + while (inputBuffer[cc] == L' ' || inputBuffer[cc] == L'\t') { + cc++; + } + + if (!_wcsnicmp(&inputBuffer[cc], L"cd", 2) || + !_wcsnicmp(&inputBuffer[cc], L"chdir", 5) || + !_wcsnicmp(&inputBuffer[cc], L"rd", 2) || + !_wcsnicmp(&inputBuffer[cc], L"rmdir", 5) || + !_wcsnicmp(&inputBuffer[cc], L"md", 2) || + !_wcsnicmp(&inputBuffer[cc], L"mkdir", 5)) { + + ret = TRUE; + } + + return ret; +} + +static void ClearConsoleCharacters(const HANDLE hOutput, const SHORT cCount) +{ + CONSOLE_SCREEN_BUFFER_INFO csbi; + DWORD written; + + GetConsoleScreenBufferInfo(hOutput, &csbi); + FillConsoleOutputCharacterW(hOutput, L' ', cCount, csbi.dwCursorPosition, &written); +} + +static DWORD GetCursorSize(const HANDLE hOutput) +{ + CONSOLE_CURSOR_INFO cursorInfo; + + if (!GetConsoleCursorInfo(hOutput, &cursorInfo)) { + cursorInfo.dwSize = 25; + } + + return cursorInfo.dwSize; +} + +static void SetCursorSize(const HANDLE hOutput, const DWORD dwSize) +{ + CONSOLE_CURSOR_INFO cursorInfo = {dwSize, TRUE}; + + SetConsoleCursorInfo(hOutput, &cursorInfo); +} + +static void SetCursorVisible(const HANDLE hOutput, const BOOL visible) +{ + CONSOLE_CURSOR_INFO cursorInfo; + + if (GetConsoleCursorInfo(hOutput, &cursorInfo)) { + cursorInfo.bVisible = visible; + SetConsoleCursorInfo(hOutput, &cursorInfo); + } +} + +/* Intended as a drop-in replacement for ReadConsole, but with tab-completion support. + */ +BOOL WCMD_ReadConsole(const HANDLE hInput, WCHAR *inputBuffer, const DWORD inputBufferLength, LPDWORD numRead, LPVOID pInputControl) +{ + HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE); + WCHAR searchstr[MAX_PATH]; + BOOL haveSearchResult = FALSE; + BOOL hadOneMatch = FALSE; + HANDLE hSearch = INVALID_HANDLE_VALUE; + WIN32_FIND_DATAW findData = {0}; + CONSOLE_SCREEN_BUFFER_INFO startConsoleInfo, consoleInfo; + DWORD numWritten, oldMode, origCursorSize; + UINT curPos; + int searchPos, insertPos, lastStartQuote; + BOOL isDirSearch; + BOOL haveMode; + BOOL haveQuotes = FALSE; + BOOL ignoreTrailingQuote = FALSE; + BOOL needQuotes = FALSE; + BOOL insert_mode = TRUE; + BOOL done = FALSE; + BOOL ret = FALSE; + + if (hInput == INVALID_HANDLE_VALUE || hOutput == INVALID_HANDLE_VALUE || !inputBuffer || !inputBufferLength) { + return FALSE; + } + + *inputBuffer = L'\0'; + curPos = 0; + + /* Get starting cursor position and size */ + GetConsoleScreenBufferInfo(hOutput, &startConsoleInfo); + origCursorSize = GetCursorSize(hOutput); + + while (!done) { + INPUT_RECORD inputRecord; + DWORD numEventsRead; + + ReadConsoleInputW(hInput, &inputRecord, 1, &numEventsRead); + if (inputRecord.EventType == KEY_EVENT && inputRecord.Event.KeyEvent.bKeyDown) { + int cc, len, oldLen; + + len = oldLen = lstrlenW(inputBuffer); + switch (inputRecord.Event.KeyEvent.wVirtualKeyCode) { + case VK_TAB: + haveSearchResult = needQuotes = FALSE; + /* If tab key was hit while cursor was not at EOL */ + if (curPos < len) { + TRACE("***** Reset search, tab hit before EOL\n"); + inputBuffer[curPos] = L'\0'; + if (hSearch != INVALID_HANDLE_VALUE) { + FindClose(hSearch); + hSearch = INVALID_HANDLE_VALUE; + } + } + /* If new search */ + if (hSearch == INVALID_HANDLE_VALUE) { + cc = searchPos = insertPos = lastStartQuote = 0; + haveQuotes = ignoreTrailingQuote = FALSE; + + /* Handle spaces in directory names. Need to quote paths if they contain spaces. + * Also, there can be multiple quoted paths on a command line, so find the current + * one. + */ + while (cc < curPos) { + if (inputBuffer[cc] == L'"') { + haveQuotes = !haveQuotes; + TRACE("Flip haveQuotes, now: %u\n", haveQuotes); + if (haveQuotes) { + lastStartQuote = cc; + } + } + cc++; + } + + /* Special case: If user entered a backslash after quotes, ignore the final quotes. */ + if (inputBuffer[curPos-2] == L'"' && inputBuffer[curPos-1] == L'\') { + TRACE("Ignore quote before trailing backslash\n"); + ignoreTrailingQuote = haveQuotes = TRUE; + } + + if (haveQuotes) { + /* Tab was hit inside a quoted path. Search specification is start of the path. */ + cc = lastStartQuote + 1; + } else { + /* Tab was hit outside of a quoted path. Find the start of this path, + * i.e. the first character after the previous spaces and tabs. + * This will be the search specification for tab-completion. + */ + cc = curPos; + while (cc > 0 && inputBuffer[cc] != L' ' && inputBuffer[cc] != L'\t') { + cc--; + } + while (cc < len && (inputBuffer[cc] == L' ' || inputBuffer[cc] == L'\t')) { + cc++; + } + } + searchPos = cc; + lstrcpynW(searchstr, &inputBuffer[searchPos], ARRAY_SIZE(searchstr) - 2); + if (ignoreTrailingQuote) { + len = lstrlenW(searchstr); + searchstr[len-2] = L'\'; + searchstr[len-1] = L'\0'; + } + lstrcatW(searchstr, L"*"); + TRACE("New search: [%s]\n", wine_dbgstr_w(searchstr)); + isDirSearch = IsDirectoryOperation(inputBuffer, inputBufferLength); +search_restart: + /* On Windows, the FindExSearchLimitToDirectories specification is merely a suggestion and an + * optimization apparently intended for network file systems. + */ + hSearch = FindFirstFileExW(searchstr, FindExInfoStandard, &findData, isDirSearch ? FindExSearchLimitToDirectories : FindExSearchNameMatch, NULL, 0); + if (hSearch != INVALID_HANDLE_VALUE) { + /* Always skip "." and ".." entries. */ + if (wcscmp(findData.cFileName, L".") && wcscmp(findData.cFileName, L"..")) { + if (!isDirSearch || findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { + haveSearchResult = TRUE; + } + } + } + } + /* Subsequent search */ + if (hSearch != INVALID_HANDLE_VALUE && !haveSearchResult) { + while (!haveSearchResult && FindNextFileW(hSearch, &findData)) { + /* Always skip "." and ".." entries. */ + if (wcscmp(findData.cFileName, L".") && wcscmp(findData.cFileName, L"..")) { + if (!isDirSearch || findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { + haveSearchResult = TRUE; + } + } + } + + if (!haveSearchResult && hadOneMatch) { + hadOneMatch = FALSE; + /* Reached the last match, start over. */ + FindClose(hSearch); + goto search_restart; + } + } + + if (haveSearchResult) { + hadOneMatch = TRUE; + + /* If this is our first time through here for this search, we need to find the insert position + * for the results. Note that this is very likely not the same location as the search position. + */ + if (!insertPos) { + /* If user entered a backslash after quotes, remove the quotes. */ + if (inputBuffer[curPos-2] == L'"' && inputBuffer[curPos-1] == L'\') { + TRACE("Remove quote before trailing backslash\n"); + inputBuffer[curPos-2] = L'\'; + inputBuffer[curPos-1] = L'\0'; + curPos--; + } + + len = lstrlenW(inputBuffer); + /* Handle paths here. Find last '\'. */ + cc = len; + /* Handle spaces in directory names. Look for '\' on first pass, then quotes on second pass + * if '\' isn't found, and finally whitespace on third pass. + */ + while (cc > searchPos && inputBuffer[cc] != L'\') { + cc--; + } + + if (cc == searchPos) { + /* Handle dir "Program Files" */ + TRACE("haveQuotes: %u, lastStartQuote: %u, curPos: %u\n", haveQuotes, lastStartQuote, curPos); + if (haveQuotes && lastStartQuote) { + cc = lastStartQuote; + } else { + cc = len; + while (cc > 0 && inputBuffer[cc] != L' ' && inputBuffer[cc] != L'\t') { + cc--; + } + } + } + + if (cc != searchPos) { + cc++; + } + + insertPos = cc; + } + + /* We have found the insert position for the results. Terminate the string here. */ + inputBuffer[insertPos] = L'\0'; + + needQuotes = wcschr(findData.cFileName, L' ') || findData.cAlternateFileName[0]; + /* Add starting quotes if needed. */ + if (needQuotes && !haveQuotes) { + len = lstrlenW(inputBuffer); + if (len < inputBufferLength - 1) { + if (searchPos <= len) { + memmove(&inputBuffer[searchPos+1], &inputBuffer[searchPos], (len - searchPos + 1) * sizeof(WCHAR)); + inputBuffer[searchPos] = L'"'; + haveQuotes = TRUE; + lastStartQuote = searchPos; + insertPos++; + } + } + } + lstrcatW(inputBuffer, findData.cFileName); + /* Add closing quotes if needed. */ + if (needQuotes || haveQuotes) { + len = lstrlenW(inputBuffer); + if (len < inputBufferLength - 1) { + inputBuffer[len] = L'"'; + inputBuffer[len+1] = L'\0'; + } + } + /* Update cursor position to end of buffer. */ + curPos = lstrlenW(inputBuffer); + } + break; + + case VK_RETURN: + if (hSearch != INVALID_HANDLE_VALUE) { + FindClose(hSearch); + hSearch = INVALID_HANDLE_VALUE; + } + TRACE("VK_RETURN: [%s]\n", wine_dbgstr_w(inputBuffer)); + /* Write current inputBuffer back to console input, then read it back. + * This will update console history. + */ + haveMode = GetConsoleMode(hInput, &oldMode); + if (haveMode) { + SetConsoleMode(hInput, ENABLE_LINE_INPUT); + } + FlushConsoleInputBuffer(hInput); + WriteLineToConsoleInput(hInput, inputBuffer, inputBufferLength); + WriteConsoleInputW(hInput, &inputRecord, 1, &numWritten); + inputRecord.Event.KeyEvent.bKeyDown = FALSE; + WriteConsoleInputW(hInput, &inputRecord, 1, &numWritten); + ret = ReadConsoleW(hInput, inputBuffer, inputBufferLength, numRead, pInputControl); + WriteConsoleW(hOutput, L"\n", 1, &numWritten, NULL); + if (haveMode) { + SetConsoleMode(hInput, oldMode); + } + SetCursorSize(hOutput, origCursorSize); + done = TRUE; + break; + + case VK_BACK: + if (len > 0) { + if (curPos < len) { + memmove(&inputBuffer[curPos-1], &inputBuffer[curPos], (len - curPos + 1) * sizeof(WCHAR)); + } + inputBuffer[len - 1] = L'\0'; + curPos -= 1; + } + if (hSearch != INVALID_HANDLE_VALUE) { + FindClose(hSearch); + hSearch = INVALID_HANDLE_VALUE; + } + break; + + case VK_DELETE: + if (len > 0) { + if (curPos < len) { + memmove(&inputBuffer[curPos], &inputBuffer[curPos+1], (len - curPos) * sizeof(WCHAR)); + } + inputBuffer[len - 1] = L'\0'; + } + if (hSearch != INVALID_HANDLE_VALUE) { + FindClose(hSearch); + hSearch = INVALID_HANDLE_VALUE; + } + break; + + case VK_ESCAPE: + /* Clear buffer and close any existing search. */ + if (len) { + /* In cases where text spans multiple console lines, FillConsoleOutputCharacter does not seem + * to be working properly so we just manually write out a string full of spaces here. + */ + memset(inputBuffer, L' ', len / sizeof(inputBuffer[0])); + SetCursorVisible(hOutput, FALSE); + SetConsoleCursorPosition(hOutput, startConsoleInfo.dwCursorPosition); + WriteConsoleW(hOutput, inputBuffer, len, &numWritten, NULL); + } + inputBuffer[0] = L'\0'; + curPos = 0; + if (hSearch != INVALID_HANDLE_VALUE) { + FindClose(hSearch); + hSearch = INVALID_HANDLE_VALUE; + haveQuotes = FALSE; + } + break; + + case VK_UP: + case VK_DOWN: + case VK_PRIOR: + case VK_NEXT: + /* Command history */ + if (len) { + /* In cases where text spans multiple console lines, FillConsoleOutputCharacter does not seem + * to be working properly so we just manually write out a string full of spaces here. + */ + memset(inputBuffer, L' ', len / sizeof(inputBuffer[0])); + SetConsoleCursorPosition(hOutput, startConsoleInfo.dwCursorPosition); + WriteConsoleW(hOutput, inputBuffer, len, &numWritten, NULL); + } + /* FIXME: Wine Console does not support obtaining command history. In order to + * support command history, Wine would need to implement GetConsoleCommandHistoryLength + * and GetConsoleCommandHistory, or we would need to implement our own history here. + * As a workaround, we chain-back to the console and let it handle history, but + * we lose tab-completion support when doing so. + */ + /* Push keystroke and pass control to console to handle history retrieval. */ + WriteConsoleInputW(hInput, &inputRecord, 1, &numWritten); + inputRecord.Event.KeyEvent.bKeyDown = FALSE; + WriteConsoleInputW(hInput, &inputRecord, 1, &numWritten); + ret = ReadConsoleW(hInput, inputBuffer, inputBufferLength, numRead, pInputControl); + done = TRUE; + break; + + case VK_LEFT: + if (curPos) { + curPos--; + } + if (inputRecord.Event.KeyEvent.dwControlKeyState & (LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED)) { + while (curPos && (inputBuffer[curPos] == L' ' || inputBuffer[curPos] == L'\t')) { + curPos--; + } + while (curPos && (inputBuffer[curPos-1] != L' ' && inputBuffer[curPos-1] != L'\t')) { + curPos--; + } + } + break; + + case VK_RIGHT: + if (inputRecord.Event.KeyEvent.dwControlKeyState & (LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED)) { + while (curPos < len && inputBuffer[curPos] != L' ' && inputBuffer[curPos] != L'\t') { + curPos++; + } + while (curPos < len && (inputBuffer[curPos] == L' ' || inputBuffer[curPos] == L'\t')) { + curPos++; + } + } else if (curPos < len) { + curPos++; + } + break; + + case VK_HOME: + curPos = 0; + break; + + case VK_END: + curPos = len; + break; + + case VK_INSERT: + insert_mode = !insert_mode; + SetCursorSize(hOutput, insert_mode ? origCursorSize : 100); + break; + + default: + /* Insert or add character to buffer */ + WCHAR ch = inputRecord.Event.KeyEvent.uChar.UnicodeChar; + if (iswprint(ch)) { + if (len < inputBufferLength - 1) { + if (insert_mode && curPos < len) { + memmove(&inputBuffer[curPos+1], &inputBuffer[curPos], (len - curPos + 1) * sizeof(WCHAR)); + } + inputBuffer[curPos++] = ch; + if (curPos > len) { + inputBuffer[curPos] = L'\0'; + } + } + + /* If user entered a delimiter then reset the search. */ + if (ch == L'\' || ch == L' ' || ch == L'\t') { + TRACE("***** Reset search, new delimiter\n"); + if (hSearch != INVALID_HANDLE_VALUE) { + FindClose(hSearch); + hSearch = INVALID_HANDLE_VALUE; + } + } + } + break; + } + + if (!done) { + len = lstrlenW(inputBuffer); + /* Update current input display in console */ + SetCursorVisible(hOutput, FALSE); + SetConsoleCursorPosition(hOutput, startConsoleInfo.dwCursorPosition); + WriteConsoleW(hOutput, inputBuffer, len, &numWritten, NULL); + if (oldLen > len) { + ClearConsoleCharacters(hOutput, oldLen - len); + } + + /* Move cursor if needed. */ + if (curPos < len) { + cc = len - curPos; + if (GetConsoleScreenBufferInfo(hOutput, &consoleInfo)) { + consoleInfo.dwCursorPosition.X -= cc; + SetConsoleCursorPosition(hOutput, consoleInfo.dwCursorPosition); + } + } + SetCursorVisible(hOutput, TRUE); + } + } + } + + return ret; +} + diff --git a/programs/cmd/wcmd.h b/programs/cmd/wcmd.h index d4adc7fd7c8..37572d07ce0 100644 --- a/programs/cmd/wcmd.h +++ b/programs/cmd/wcmd.h @@ -212,6 +212,7 @@ WCHAR *WCMD_LoadMessage(UINT id); WCHAR *WCMD_strsubstW(WCHAR *start, const WCHAR* next, const WCHAR* insert, int len); RETURN_CODE WCMD_wait_for_input(HANDLE hIn); BOOL WCMD_ReadFile(const HANDLE hIn, WCHAR *intoBuf, const DWORD maxChars, LPDWORD charsRead); +BOOL WCMD_ReadConsole(const HANDLE hInput, WCHAR *inputBuffer, const DWORD inputBufferLength, LPDWORD numRead, LPVOID pInputControl);
enum read_parse_line {RPL_SUCCESS, RPL_EOF, RPL_SYNTAXERROR}; enum read_parse_line WCMD_ReadAndParseLine(const WCHAR *initialcmd, CMD_NODE **output);
thanks for looking into that.
but:
* this should be implemented using CONSOLE_READCONSOLE_CONTROL parameter to ReadConsole (so that you don't have to bother about line editing nor history) * from what I see on Windows, the looping around filenames is done whatever the command (and even at start of input) * this should also support shift-tab to circle backwards
this also conflicts with [MR!7843](https://gitlab.winehq.org/wine/wine/-/merge_requests/7843/diffs) (which shall pave the way to using CONSOLE_READCONSOLE_CONTROL)
On Thu Apr 17 10:09:16 2025 +0000, eric pouech wrote:
thanks for looking into that. but:
- this should be implemented using CONSOLE_READCONSOLE_CONTROL parameter
to ReadConsole (so that you don't have to bother about line editing nor history)
- from what I see on Windows, the looping around filenames is done
whatever the command (and even at start of input)
- this should also support shift-tab to circle backwards
this also conflicts with [MR!7843](https://gitlab.winehq.org/wine/wine/-/merge_requests/7843/diffs) (which shall pave the way to using CONSOLE_READCONSOLE_CONTROL)
You referenced this MR in your comment. I think you wanted to reference !7586 instead.
On Thu Apr 17 10:09:16 2025 +0000, Loïc Rebmeister wrote:
You referenced this MR in your comment. I think you wanted to reference !7586 instead.
yes thanks...
On Thu Apr 17 12:57:02 2025 +0000, eric pouech wrote:
yes thanks...
Thanks for the feedback. It wasn't clear to me how CONSOLE_READCONSOLE_CONTROL worked but now that I have looked at the docs for it, I see.
Regarding the second point, this code already does what you seem to claim it does not. It supports tab-completion at any point in the command line. For example, if the user enters 'not' at C:\windows and then pressed the tab key, it is tab-completed to 'notepad.exe'. It also cycles through files and directories if the tab key is pressed at an empty command line.
If this work is conflicting with other work that is ongoing, I can withdraw the request. Or, I can look into using CONSOLE_READCONSOLE_CONTROL as you suggest. I see how using that would support obtaining command line history, but I don't see how that would allow insertion of the new entered command into console's history if ReadConsole returns early due to tab key being pressed.
On Thu Apr 17 15:19:52 2025 +0000, Joe Souza wrote:
Thanks for the feedback. It wasn't clear to me how CONSOLE_READCONSOLE_CONTROL worked but now that I have looked at the docs for it, I see. Regarding the second point, this code already does what you seem to claim it does not. It supports tab-completion at any point in the command line. For example, if the user enters 'not' at C:\windows and then pressed the tab key, it is tab-completed to 'notepad.exe'. It also cycles through files and directories if the tab key is pressed at an empty command line. If this work is conflicting with other work that is ongoing, I can withdraw the request. Or, I can look into using CONSOLE_READCONSOLE_CONTROL as you suggest. I see how using that would support obtaining command line history, but I don't see how that would allow insertion of the new entered command into console's history if ReadConsole returns early due to tab key being pressed.
It's also not clear to me how the user would continue editing the command line if ReadConsole were to return early (if tab pressed) when using CONSOLE_READCONSOLE_CONTROL.
the logic of using CONSOLE_READCONSOLE_CONTROL is that it either returns when:
* ENTER is hit (as it would do if that parameter was NULL) * or with a status saying that tab has been entered; at that point it's up to caller to update the the input buffer and call again ReadInput for the user to continue editing the line
if you're interested I shall have somewhere an early implementation of this that I wrote when implementing the console counterpart (likely it no longer applies cleanly in cmd, but the logic should still be relevant - don't remember why I didn't send it, or maybe it didn't fully work on unix console)
regarding the other MR, since ctrl-c handling in CONSOLE_READCONSOLE_CONTROL will require updating the console layer, I'd suggest not to wait on it, but you could integrate the remarks I made on how to call ReadInpuyt with CONSOLE_READCONSOLE_CONSOLE so the two MR would benefit from it
Regarding the second point, this code already does what you seem to claim it does not. It supports tab-completion at any point in the command line. For example, if the user enters 'not' at C:\windows and then pressed the tab key, it is tab-completed to 'notepad.exe'. It also cycles through files and directories if the tab key is pressed at an empty command line.
actually I was reacting the code in `IsDirectoryOperation()` which doesn't seem to exist in native
On Thu Apr 17 17:44:58 2025 +0000, eric pouech wrote:
the logic of using CONSOLE_READCONSOLE_CONTROL is that it either returns when:
- ENTER is hit (as it would do if that parameter was NULL)
- or with a status saying that tab has been entered; at that point it's
up to caller to update the input buffer and call again ReadInput for the user to continue editing the line if you're interested I shall have somewhere an early implementation of this that I wrote when implementing the console counterpart (likely it no longer applies cleanly in cmd, but the logic should still be relevant
- don't remember why I didn't send it, or maybe it didn't fully work on
unix console) regarding the other MR, since ctrl-c handling in CONSOLE_READCONSOLE_CONTROL will require updating the console layer, I'd suggest not to wait on it, but you could integrate the remarks I made on how to call ReadInpuyt with CONSOLE_READCONSOLE_CONSOLE so the two MR would benefit from it
Regarding the second point, this code already does what you seem to
claim it does not. It supports tab-completion at any point in the command line. For example, if the user enters 'not' at C:\windows and then pressed the tab key, it is tab-completed to 'notepad.exe'. It also cycles through files and directories if the tab key is pressed at an empty command line. actually I was reacting the code in `IsDirectoryOperation()` which doesn't seem to exist in native
Thank you. Regarding IsDirectoryOperation, this is not an external API but rather an internal function. Windows definitely cycles through directories only, and not files, if the command being entered on the line is a directory operation like CD or MD, etc. For all other cases, and even for the command itself and at an empty command prompt, etc., the code will cycle through files and directories.
Regarding calling ReadConsole/CONSOLE_READCONSOLE_CONTROL, updating the buffer and then calling ReadConsoleInput, it seems we then still need the line editor code. It seems that the only benefit to that method would be mainly in the case where the user never uses the tab key. Otherwise I think that most of my code here would still remain.
Thank you. Regarding IsDirectoryOperation, this is not an external API but rather an internal function. Windows definitely cycles through directories only, and not files, if the command being entered on the line is a directory operation like CD or MD, etc. For all other cases, and even for the command itself and at an empty command prompt, etc., the code will cycle through files and directories.
I missed that...
Regarding calling ReadConsole/CONSOLE_READCONSOLE_CONTROL, updating the buffer and then calling ReadConsoleInput, it seems we then still need the line editor code. It seems that the only benefit to that method would be mainly in the case where the user never uses the tab key. Otherwise I think that most of my code here would still remain.
the line edit part should still be handled by ReadInput
yes, some of the code you wrote will be needed: redraw (like you do for cleaning end of line... multiline support), cursor (re)positionning
but you should not need any code dealing with INPUT_RECORD
On Fri Apr 18 07:21:30 2025 +0000, eric pouech wrote:
Thank you. Regarding IsDirectoryOperation, this is not an external API
but rather an internal function. Windows definitely cycles through directories only, and not files, if the command being entered on the line is a directory operation like CD or MD, etc. For all other cases, and even for the command itself and at an empty command prompt, etc., the code will cycle through files and directories. I missed that...
Regarding calling ReadConsole/CONSOLE_READCONSOLE_CONTROL, updating
the buffer and then calling ReadConsoleInput, it seems we then still need the line editor code. It seems that the only benefit to that method would be mainly in the case where the user never uses the tab key. Otherwise I think that most of my code here would still remain. the line edit part should still be handled by ReadInput yes, some of the code you wrote will be needed: redraw (like you do for cleaning end of line... multiline support), cursor (re)positionning but you should not need any code dealing with INPUT_RECORD
I'm not finding any function in the code called ReadInput.
ReadConsole() sorry
On Fri Apr 18 15:34:20 2025 +0000, eric pouech wrote:
ReadConsole() sorry
Can ReadConsole() accept a buffer with pre-filled text for further editing? From what I read in docs online, it behaves similar to ReadFile() which is input-only. I'll try to test some code on Windows to verify behaviour.
On Fri Apr 18 15:34:20 2025 +0000, Joe Souza wrote:
Can ReadConsole() accept a buffer with pre-filled text for further editing? From what I read in docs online, it behaves similar to ReadFile() which is input-only. I'll try to test some code on Windows to verify behaviour.
OK, using ReadConsole/CONSOLE_READCONSOLE_CONTROL on Windows gives me the behaviour that I need; i.e. pre-filled text is preserved and can be further edited. Caller must output the text first, but seems to work as expected.
I'll start making the changes to my code soon, within the coming days.
also note that for the first word of command line, when using tab, native cmd.exe filters on directories and files that can be executed (likely using PATHEXT list of extensions, didn't test that though)
i'd suggest handling in two seperate patches the rd/md part, and the first parameter one...
that should boil down to adding filters to the files gotten out of find files (directories and/or extensions) depending on context